Introduce Virtual Input Device Driver

Add the new section of input device driver, vinput[1].

Also, update the Acknowledgements.

[1] https://github.com/sysprog21/vinput
This commit is contained in:
linD026 2022-04-05 23:51:54 +08:00
parent 85ee0ec67b
commit 636c1e702d
11 changed files with 656 additions and 2 deletions

View File

@ -1,2 +1,3 @@
bottomhalf bottomhalf
intrpt intrpt
vkbd

View File

@ -6,3 +6,4 @@ Jim Huang <jserv.tw@gmail.com> Jim Huang <jserv@biilabs.io>
linD026 <shiyn.lin@gmail.com> linD026 <66012716+linD026@users.noreply.github.com> linD026 <shiyn.lin@gmail.com> linD026 <66012716+linD026@users.noreply.github.com>
linD026 <shiyn.lin@gmail.com> linD026 <0086d026@email.ntou.edu.tw> linD026 <shiyn.lin@gmail.com> linD026 <0086d026@email.ntou.edu.tw>
linD026 <shiyn.lin@gmail.com> linzhien <0086d026@email.ntou.edu.tw> linD026 <shiyn.lin@gmail.com> linzhien <0086d026@email.ntou.edu.tw>
mengxinayan <31788564+mengxinayan@users.noreply.github.com> 萌新阿岩 <31788564+mengxinayan@users.noreply.github.com>

View File

@ -3,9 +3,11 @@
Arush Sharma, % <46960231+arushsharma24@users.noreply.github.com> Arush Sharma, % <46960231+arushsharma24@users.noreply.github.com>
asas1asas200, % <asas1asas200@gmail.com> asas1asas200, % <asas1asas200@gmail.com>
Benno Bielmeier, % <32938211+bbenno@users.noreply.github.com> Benno Bielmeier, % <32938211+bbenno@users.noreply.github.com>
Bob Lee, % <defru04002@gmail.com>
Brad Baker, % <brad@brdbkr.com> Brad Baker, % <brad@brdbkr.com>
ccs100203, % <ccs100203@gmail.com> ccs100203, % <ccs100203@gmail.com>
Chih-Yu Chen, % <34228283+chihyu1206@users.noreply.github.com> Chih-Yu Chen, % <34228283+chihyu1206@users.noreply.github.com>
Ching-Hua (Vivian) Lin, % <jkrvivian@gmail.com>
ChinYikMing, % <yikming2222@gmail.com> ChinYikMing, % <yikming2222@gmail.com>
Cyril Brulebois, % <cyril@debamax.com> Cyril Brulebois, % <cyril@debamax.com>
Daniele Paolo Scarpazza, % <> Daniele Paolo Scarpazza, % <>
@ -22,10 +24,15 @@ Hsin-Hsiang Peng, % <hsinspeng@gmail.com>
Ignacio Martin, % <> Ignacio Martin, % <>
JianXing Wu, % <fdgkhdkgh@gmail.com> JianXing Wu, % <fdgkhdkgh@gmail.com>
linD026, % <shiyn.lin@gmail.com> linD026, % <shiyn.lin@gmail.com>
lyctw, % <lyctw.ee@gmail.com>
manbing, % <manbing3@gmail.com>
Marconi Jiang, % <marconi1964@yahoo.com> Marconi Jiang, % <marconi1964@yahoo.com>
mengxinayan, % <31788564+mengxinayan@users.noreply.github.com>
RinHizakura, % <s921975628@gmail.com> RinHizakura, % <s921975628@gmail.com>
Roman Lakeev, % <> Roman Lakeev, % <>
Stacy Prowell, % <sprowell@gmail.com> Stacy Prowell, % <sprowell@gmail.com>
Steven Lung, % <1030steven@gmail.com>
Tristan Lelong, % <tristan.lelong@blunderer.org>
Tucker Polomik, % <tucker.polomik@inficon.com> Tucker Polomik, % <tucker.polomik@inficon.com>
VxTeemo, % <tcccvvv123@gmail.com> VxTeemo, % <tcccvvv123@gmail.com>
Wei-Lun Tsai, % <alan23273850@gmail.com> Wei-Lun Tsai, % <alan23273850@gmail.com>

View File

@ -56,6 +56,10 @@ DisableFormat: false
ExperimentalAutoDetectBinPacking: false ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false FixNamespaceComments: false
ForEachMacros:
- 'list_for_each'
- 'list_for_each_safe'
IncludeBlocks: Preserve IncludeBlocks: Preserve
IncludeCategories: IncludeCategories:
- Regex: '.*' - Regex: '.*'

View File

@ -29,6 +29,8 @@ obj-m += example_atomic.o
obj-m += example_mutex.o obj-m += example_mutex.o
obj-m += bottomhalf.o obj-m += bottomhalf.o
obj-m += ioctl.o obj-m += ioctl.o
obj-m += vinput.o
obj-m += vkbd.o
PWD := $(CURDIR) PWD := $(CURDIR)

398
examples/vinput.c Normal file
View File

@ -0,0 +1,398 @@
#include <linux/cdev.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <asm/uaccess.h>
#include "vinput.h"
#define DRIVER_NAME "vinput"
#define dev_to_vinput(dev) container_of(dev, struct vinput, dev)
static DECLARE_BITMAP(vinput_ids, VINPUT_MINORS);
static LIST_HEAD(vinput_devices);
static LIST_HEAD(vinput_vdevices);
static int vinput_dev;
static struct spinlock vinput_lock;
static struct class vinput_class;
/* Search the name of vinput device in the vinput_devices linked list,
* which added at vinput_register().
*/
static struct vinput_device *vinput_get_device_by_type(const char *type)
{
int found = 0;
struct vinput_device *vinput;
struct list_head *curr;
spin_lock(&vinput_lock);
list_for_each (curr, &vinput_devices) {
vinput = list_entry(curr, struct vinput_device, list);
if (vinput && strncmp(type, vinput->name, strlen(vinput->name)) == 0) {
found = 1;
break;
}
}
spin_unlock(&vinput_lock);
if (found)
return vinput;
return ERR_PTR(-ENODEV);
}
/* Search the id of virtual device in the vinput_vdevices linked list,
* which added at vinput_alloc_vdevice().
*/
static struct vinput *vinput_get_vdevice_by_id(long id)
{
struct vinput *vinput = NULL;
struct list_head *curr;
spin_lock(&vinput_lock);
list_for_each (curr, &vinput_vdevices) {
vinput = list_entry(curr, struct vinput, list);
if (vinput && vinput->id == id)
break;
}
spin_unlock(&vinput_lock);
if (vinput && vinput->id == id)
return vinput;
return ERR_PTR(-ENODEV);
}
static int vinput_open(struct inode *inode, struct file *file)
{
int err = 0;
struct vinput *vinput = NULL;
vinput = vinput_get_vdevice_by_id(iminor(inode));
if (IS_ERR(vinput))
err = PTR_ERR(vinput);
else
file->private_data = vinput;
return err;
}
static int vinput_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t vinput_read(struct file *file, char __user *buffer, size_t count,
loff_t *offset)
{
int len;
char buff[VINPUT_MAX_LEN + 1];
struct vinput *vinput = file->private_data;
len = vinput->type->ops->read(vinput, buff, count);
if (*offset > len)
count = 0;
else if (count + *offset > VINPUT_MAX_LEN)
count = len - *offset;
if (raw_copy_to_user(buffer, buff + *offset, count))
count = -EFAULT;
*offset += count;
return count;
}
static ssize_t vinput_write(struct file *file, const char __user *buffer,
size_t count, loff_t *offset)
{
char buff[VINPUT_MAX_LEN + 1];
struct vinput *vinput = file->private_data;
memset(buff, 0, sizeof(char) * (VINPUT_MAX_LEN + 1));
if (count > VINPUT_MAX_LEN) {
dev_warn(&vinput->dev, "Too long. %d bytes allowed\n", VINPUT_MAX_LEN);
return -EINVAL;
}
if (raw_copy_from_user(buff, buffer, count))
return -EFAULT;
return vinput->type->ops->send(vinput, buff, count);
}
static const struct file_operations vinput_fops = {
.owner = THIS_MODULE,
.open = vinput_open,
.release = vinput_release,
.read = vinput_read,
.write = vinput_write,
};
static void vinput_unregister_vdevice(struct vinput *vinput)
{
input_unregister_device(vinput->input);
if (vinput->type->ops->kill)
vinput->type->ops->kill(vinput);
}
static void vinput_destroy_vdevice(struct vinput *vinput)
{
/* Remove from the list first */
spin_lock(&vinput_lock);
list_del(&vinput->list);
clear_bit(vinput->id, vinput_ids);
spin_unlock(&vinput_lock);
module_put(THIS_MODULE);
kfree(vinput);
}
static void vinput_release_dev(struct device *dev)
{
struct vinput *vinput = dev_to_vinput(dev);
int id = vinput->id;
vinput_destroy_vdevice(vinput);
pr_debug("released vinput%d.\n", id);
}
static struct vinput *vinput_alloc_vdevice(void)
{
int err;
struct vinput *vinput = kzalloc(sizeof(struct vinput), GFP_KERNEL);
try_module_get(THIS_MODULE);
memset(vinput, 0, sizeof(struct vinput));
spin_lock_init(&vinput->lock);
spin_lock(&vinput_lock);
vinput->id = find_first_zero_bit(vinput_ids, VINPUT_MINORS);
if (vinput->id >= VINPUT_MINORS) {
err = -ENOBUFS;
goto fail_id;
}
set_bit(vinput->id, vinput_ids);
list_add(&vinput->list, &vinput_vdevices);
spin_unlock(&vinput_lock);
/* allocate the input device */
vinput->input = input_allocate_device();
if (vinput->input == NULL) {
pr_err("vinput: Cannot allocate vinput input device\n");
err = -ENOMEM;
goto fail_input_dev;
}
/* initialize device */
vinput->dev.class = &vinput_class;
vinput->dev.release = vinput_release_dev;
vinput->dev.devt = MKDEV(vinput_dev, vinput->id);
dev_set_name(&vinput->dev, DRIVER_NAME "%lu", vinput->id);
return vinput;
fail_input_dev:
spin_lock(&vinput_lock);
list_del(&vinput->list);
fail_id:
spin_unlock(&vinput_lock);
module_put(THIS_MODULE);
kfree(vinput);
return ERR_PTR(err);
}
static int vinput_register_vdevice(struct vinput *vinput)
{
int err = 0;
/* register the input device */
vinput->input->name = vinput->type->name;
vinput->input->phys = "vinput";
vinput->input->dev.parent = &vinput->dev;
vinput->input->id.bustype = BUS_VIRTUAL;
vinput->input->id.product = 0x0000;
vinput->input->id.vendor = 0x0000;
vinput->input->id.version = 0x0000;
err = vinput->type->ops->init(vinput);
if (err == 0)
dev_info(&vinput->dev, "Registered virtual input %s %ld\n",
vinput->type->name, vinput->id);
return err;
}
static ssize_t export_store(struct class *class, struct class_attribute *attr,
const char *buf, size_t len)
{
int err;
struct vinput *vinput;
struct vinput_device *device;
device = vinput_get_device_by_type(buf);
if (IS_ERR(device)) {
pr_info("vinput: This virtual device isn't registered\n");
err = PTR_ERR(device);
goto fail;
}
vinput = vinput_alloc_vdevice();
if (IS_ERR(vinput)) {
err = PTR_ERR(vinput);
goto fail;
}
vinput->type = device;
err = device_register(&vinput->dev);
if (err < 0)
goto fail_register;
err = vinput_register_vdevice(vinput);
if (err < 0)
goto fail_register_vinput;
return len;
fail_register_vinput:
device_unregister(&vinput->dev);
fail_register:
vinput_destroy_vdevice(vinput);
fail:
return err;
}
/* This macro generates class_attr_export structure and export_store() */
static CLASS_ATTR_WO(export);
static ssize_t unexport_store(struct class *class, struct class_attribute *attr,
const char *buf, size_t len)
{
int err;
unsigned long id;
struct vinput *vinput;
err = kstrtol(buf, 10, &id);
if (err) {
err = -EINVAL;
goto failed;
}
vinput = vinput_get_vdevice_by_id(id);
if (IS_ERR(vinput)) {
pr_err("vinput: No such vinput device %ld\n", id);
err = PTR_ERR(vinput);
goto failed;
}
vinput_unregister_vdevice(vinput);
device_unregister(&vinput->dev);
return len;
failed:
return err;
}
/* This macro generates class_attr_unexport structure and unexport_store() */
static CLASS_ATTR_WO(unexport);
static struct attribute *vinput_class_attrs[] = {
&class_attr_export.attr,
&class_attr_unexport.attr,
NULL,
};
/* This macro generates vinput_class_groups structure */
ATTRIBUTE_GROUPS(vinput_class);
static struct class vinput_class = {
.name = "vinput",
.owner = THIS_MODULE,
.class_groups = vinput_class_groups,
};
int vinput_register(struct vinput_device *dev)
{
spin_lock(&vinput_lock);
list_add(&dev->list, &vinput_devices);
spin_unlock(&vinput_lock);
pr_info("vinput: registered new virtual input device '%s'\n", dev->name);
return 0;
}
EXPORT_SYMBOL(vinput_register);
void vinput_unregister(struct vinput_device *dev)
{
struct list_head *curr, *next;
/* Remove from the list first */
spin_lock(&vinput_lock);
list_del(&dev->list);
spin_unlock(&vinput_lock);
/* unregister all devices of this type */
list_for_each_safe (curr, next, &vinput_vdevices) {
struct vinput *vinput = list_entry(curr, struct vinput, list);
if (vinput && vinput->type == dev) {
vinput_unregister_vdevice(vinput);
device_unregister(&vinput->dev);
}
}
pr_info("vinput: unregistered virtual input device '%s'\n", dev->name);
}
EXPORT_SYMBOL(vinput_unregister);
static int __init vinput_init(void)
{
int err = 0;
pr_info("vinput: Loading virtual input driver\n");
vinput_dev = register_chrdev(0, DRIVER_NAME, &vinput_fops);
if (vinput_dev < 0) {
pr_err("vinput: Unable to allocate char dev region\n");
goto failed_alloc;
}
spin_lock_init(&vinput_lock);
err = class_register(&vinput_class);
if (err < 0) {
pr_err("vinput: Unable to register vinput class\n");
goto failed_class;
}
return 0;
failed_class:
class_unregister(&vinput_class);
failed_alloc:
return err;
}
static void __exit vinput_end(void)
{
pr_info("vinput: Unloading virtual input driver\n");
unregister_chrdev(vinput_dev, DRIVER_NAME);
class_unregister(&vinput_class);
}
module_init(vinput_init);
module_exit(vinput_end);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Emulate input events");

45
examples/vinput.h Normal file
View File

@ -0,0 +1,45 @@
#ifndef VINPUT_H
#define VINPUT_H
#include <linux/input.h>
#include <linux/spinlock.h>
#define VINPUT_MAX_LEN 128
#define MAX_VINPUT 32
#define VINPUT_MINORS MAX_VINPUT
#define dev_to_vinput(dev) container_of(dev, struct vinput, dev)
struct vinput_device;
struct vinput {
long id;
long devno;
long last_entry;
spinlock_t lock;
void *priv_data;
struct device dev;
struct list_head list;
struct input_dev *input;
struct vinput_device *type;
};
struct vinput_ops {
int (*init)(struct vinput *);
int (*kill)(struct vinput *);
int (*send)(struct vinput *, char *, int);
int (*read)(struct vinput *, char *, int);
};
struct vinput_device {
char name[16];
struct list_head list;
struct vinput_ops *ops;
};
int vinput_register(struct vinput_device *dev);
void vinput_unregister(struct vinput_device *dev);
#endif

106
examples/vkbd.c Normal file
View File

@ -0,0 +1,106 @@
#include <linux/init.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include "vinput.h"
#define VINPUT_KBD "vkbd"
#define VINPUT_RELEASE 0
#define VINPUT_PRESS 1
static unsigned short vkeymap[KEY_MAX];
static int vinput_vkbd_init(struct vinput *vinput)
{
int i;
/* Set up the input bitfield */
vinput->input->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
vinput->input->keycodesize = sizeof(unsigned short);
vinput->input->keycodemax = KEY_MAX;
vinput->input->keycode = vkeymap;
for (i = 0; i < KEY_MAX; i++)
set_bit(vkeymap[i], vinput->input->keybit);
/* vinput will help us allocate new input device structure via
* input_allocate_device(). So, we can register it straightforwardly.
*/
return input_register_device(vinput->input);
}
static int vinput_vkbd_read(struct vinput *vinput, char *buff, int len)
{
spin_lock(&vinput->lock);
len = snprintf(buff, len, "%+ld\n", vinput->last_entry);
spin_unlock(&vinput->lock);
return len;
}
static int vinput_vkbd_send(struct vinput *vinput, char *buff, int len)
{
int ret;
long key = 0;
short type = VINPUT_PRESS;
/* Determine which event was received (press or release)
* and store the state.
*/
if (buff[0] == '+')
ret = kstrtol(buff + 1, 10, &key);
else
ret = kstrtol(buff, 10, &key);
if (ret)
dev_err(&vinput->dev, "error during kstrtol: -%d\n", ret);
spin_lock(&vinput->lock);
vinput->last_entry = key;
spin_unlock(&vinput->lock);
if (key < 0) {
type = VINPUT_RELEASE;
key = -key;
}
dev_info(&vinput->dev, "Event %s code %ld\n",
(type == VINPUT_RELEASE) ? "VINPUT_RELEASE" : "VINPUT_PRESS", key);
/* Report the state received to input subsystem. */
input_report_key(vinput->input, key, type);
/* Tell input subsystem that it finished the report. */
input_sync(vinput->input);
return len;
}
static struct vinput_ops vkbd_ops = {
.init = vinput_vkbd_init,
.send = vinput_vkbd_send,
.read = vinput_vkbd_read,
};
static struct vinput_device vkbd_dev = {
.name = VINPUT_KBD,
.ops = &vkbd_ops,
};
static int __init vkbd_init(void)
{
int i;
for (i = 0; i < KEY_MAX; i++)
vkeymap[i] = i;
return vinput_register(&vkbd_dev);
}
static void __exit vkbd_end(void)
{
vinput_unregister(&vkbd_dev);
}
module_init(vkbd_init);
module_exit(vkbd_end);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Emulate keyboard input events through /dev/vinput");

View File

@ -1857,6 +1857,88 @@ Here is an example of symmetrically encrypting a string using the AES algorithm
\samplec{examples/cryptosk.c} \samplec{examples/cryptosk.c}
\section{Virtual Input Device Driver}
\label{sec:vinput}
The input device driver is a module that provides a way to communicate with the interaction device via the event.
For example, the keyboard can send the press or release event to tell the kernel what we want to do.
The input device driver will allocate a new input structure with \cpp|input_allocate_device()| and sets up input bitfields, device id, version, etc.
After that, registers it by calling \cpp|input_register_device()|.
Here is an example, vinput,
It is an API to allow easy development of virtual input drivers.
The drivers needs to export a \cpp|vinput_device()| that contains the virtual device name and \cpp|vinput_ops| structure that describes:
\begin{itemize}
\item the init function: \cpp|init()|
\item the input event injection function: \cpp|send()|
\item the readback function: \cpp|read()|
\end{itemize}
Then using \cpp|vinput_register_device()| and \cpp|vinput_unregister_device()| will add a new device to the list of support virtual input devices.
\begin{code}
int init(struct vinput *);
\end{code}
This function is passed a \cpp|struct vinput| already initialized with an allocated \cpp|struct input_dev|.
The \cpp|init()| function is responsible for initializing the capabilities of the input device and register it.
\begin{code}
int send(struct vinput *, char *, int);
\end{code}
This function will receive a user string to interpret and inject the event using the \cpp|input_report_XXXX| or \cpp|input_event| call.
The string is already copied from user.
\begin{code}
int read(struct vinput *, char *, int);
\end{code}
This function is used for debugging and should fill the buffer parameter with the last event sent in the virtual input device format.
The buffer will then be copied to user.
vinput devices are created and destroyed using sysfs.
And, event injection is done through a \verb|/dev| node.
The device name will be used by the userland to export a new virtual input device.
To create a \verb|vinputX| sysfs entry and \verb|/dev| node.
\begin{codebash}
echo "vkbd" | sudo tee /sys/class/vinput/export
\end{codebash}
To unexport the device, just echo its id in unexport:
\begin{codebash}
echo "0" | sudo tee /sys/class/vinput/unexport
\end{codebash}
\samplec{examples/vinput.h}
\samplec{examples/vinput.c}
Here the virtual keyboard is one of example to use vinput.
It supports all \cpp|KEY_MAX| keycodes.
The injection format is the \cpp|KEY_CODE| such as defined in \src{include/linux/input.h}.
A positive value means \cpp|KEY_PRESS| while a negative value is a \cpp|KEY_RELEASE|.
The keyboard supports repetition when the key stays pressed for too long.
The following demonstrates how simulation work.
Simulate a key press on "g" (\cpp|KEY_G| = 34):
\begin{codebash}
echo "+34" | sudo tee /dev/vinput0
\end{codebash}
Simulate a key release on "g" (\cpp|KEY_G| = 34:)
\begin{codebash}
echo "-34" | sudo tee /dev/vinput0
\end{codebash}
\samplec{examples/vkbd.c}
% TODO: Add description of attribute
% TODO: Add vts.c and vmouse.c example
\section{Standardizing the interfaces: The Device Model} \section{Standardizing the interfaces: The Device Model}
\label{sec:device_model} \label{sec:device_model}
Up to this point we have seen all kinds of modules doing all kinds of things, but there was no consistency in their interfaces with the rest of the kernel. Up to this point we have seen all kinds of modules doing all kinds of things, but there was no consistency in their interfaces with the rest of the kernel.

View File

@ -3,9 +3,11 @@
Arush Sharma,<46960231+arushsharma24@users.noreply.github.com> Arush Sharma,<46960231+arushsharma24@users.noreply.github.com>
asas1asas200,<asas1asas200@gmail.com> asas1asas200,<asas1asas200@gmail.com>
Benno Bielmeier,<32938211+bbenno@users.noreply.github.com> Benno Bielmeier,<32938211+bbenno@users.noreply.github.com>
Bob Lee,<defru04002@gmail.com>
Brad Baker,<brad@brdbkr.com> Brad Baker,<brad@brdbkr.com>
ccs100203,<ccs100203@gmail.com> ccs100203,<ccs100203@gmail.com>
Chih-Yu Chen,<34228283+chihyu1206@users.noreply.github.com> Chih-Yu Chen,<34228283+chihyu1206@users.noreply.github.com>
Ching-Hua (Vivian) Lin,<jkrvivian@gmail.com>
ChinYikMing,<yikming2222@gmail.com> ChinYikMing,<yikming2222@gmail.com>
Cyril Brulebois,<cyril@debamax.com> Cyril Brulebois,<cyril@debamax.com>
Daniele Paolo Scarpazza,<> Daniele Paolo Scarpazza,<>
@ -22,10 +24,15 @@ Hsin-Hsiang Peng,<hsinspeng@gmail.com>
Ignacio Martin,<> Ignacio Martin,<>
JianXing Wu,<fdgkhdkgh@gmail.com> JianXing Wu,<fdgkhdkgh@gmail.com>
linD026,<shiyn.lin@gmail.com>,<0086d026@email.ntou.edu.tw>,<66012716+linD026@users.noreply.github.com> linD026,<shiyn.lin@gmail.com>,<0086d026@email.ntou.edu.tw>,<66012716+linD026@users.noreply.github.com>
lyctw,<lyctw.ee@gmail.com>
manbing,<manbing3@gmail.com>
Marconi Jiang,<marconi1964@yahoo.com> Marconi Jiang,<marconi1964@yahoo.com>
mengxinayan,<31788564+mengxinayan@users.noreply.github.com>
RinHizakura,<s921975628@gmail.com> RinHizakura,<s921975628@gmail.com>
Roman Lakeev,<> Roman Lakeev,<>
Stacy Prowell,<sprowell@gmail.com> Stacy Prowell,<sprowell@gmail.com>
Steven Lung,<1030steven@gmail.com>
Tristan Lelong,<tristan.lelong@blunderer.org>
Tucker Polomik,<tucker.polomik@inficon.com> Tucker Polomik,<tucker.polomik@inficon.com>
VxTeemo,<tcccvvv123@gmail.com> VxTeemo,<tcccvvv123@gmail.com>
Wei-Lun Tsai,<alan23273850@gmail.com> Wei-Lun Tsai,<alan23273850@gmail.com>

View File

@ -5,3 +5,4 @@ Francois Audeon,<>
Horst Schirmeier,<> Horst Schirmeier,<>
Ignacio Martin,<> Ignacio Martin,<>
Roman Lakeev,<> Roman Lakeev,<>
Tristan Lelong,<tristan.lelong@blunderer.org>