548 lines
14 KiB
C
548 lines
14 KiB
C
|
// SPDX-License-Identifier: GPL-2.0-only
|
||
|
/*
|
||
|
* WWAN device simulator for WWAN framework testing.
|
||
|
*
|
||
|
* Copyright (c) 2021, Sergey Ryazanov <ryazanov.s.a@gmail.com>
|
||
|
*/
|
||
|
|
||
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
||
|
|
||
|
#include <linux/kernel.h>
|
||
|
#include <linux/module.h>
|
||
|
#include <linux/slab.h>
|
||
|
#include <linux/device.h>
|
||
|
#include <linux/spinlock.h>
|
||
|
#include <linux/list.h>
|
||
|
#include <linux/skbuff.h>
|
||
|
#include <linux/netdevice.h>
|
||
|
#include <linux/wwan.h>
|
||
|
#include <linux/debugfs.h>
|
||
|
#include <linux/workqueue.h>
|
||
|
|
||
|
#include <net/arp.h>
|
||
|
|
||
|
static int wwan_hwsim_devsnum = 2;
|
||
|
module_param_named(devices, wwan_hwsim_devsnum, int, 0444);
|
||
|
MODULE_PARM_DESC(devices, "Number of simulated devices");
|
||
|
|
||
|
static struct class *wwan_hwsim_class;
|
||
|
|
||
|
static struct dentry *wwan_hwsim_debugfs_topdir;
|
||
|
static struct dentry *wwan_hwsim_debugfs_devcreate;
|
||
|
|
||
|
static DEFINE_SPINLOCK(wwan_hwsim_devs_lock);
|
||
|
static LIST_HEAD(wwan_hwsim_devs);
|
||
|
static unsigned int wwan_hwsim_dev_idx;
|
||
|
|
||
|
struct wwan_hwsim_dev {
|
||
|
struct list_head list;
|
||
|
unsigned int id;
|
||
|
struct device dev;
|
||
|
struct work_struct del_work;
|
||
|
struct dentry *debugfs_topdir;
|
||
|
struct dentry *debugfs_portcreate;
|
||
|
spinlock_t ports_lock; /* Serialize ports creation/deletion */
|
||
|
unsigned int port_idx;
|
||
|
struct list_head ports;
|
||
|
};
|
||
|
|
||
|
struct wwan_hwsim_port {
|
||
|
struct list_head list;
|
||
|
unsigned int id;
|
||
|
struct wwan_hwsim_dev *dev;
|
||
|
struct wwan_port *wwan;
|
||
|
struct work_struct del_work;
|
||
|
struct dentry *debugfs_topdir;
|
||
|
enum { /* AT command parser state */
|
||
|
AT_PARSER_WAIT_A,
|
||
|
AT_PARSER_WAIT_T,
|
||
|
AT_PARSER_WAIT_TERM,
|
||
|
AT_PARSER_SKIP_LINE,
|
||
|
} pstate;
|
||
|
};
|
||
|
|
||
|
static const struct file_operations wwan_hwsim_debugfs_portdestroy_fops;
|
||
|
static const struct file_operations wwan_hwsim_debugfs_portcreate_fops;
|
||
|
static const struct file_operations wwan_hwsim_debugfs_devdestroy_fops;
|
||
|
static void wwan_hwsim_port_del_work(struct work_struct *work);
|
||
|
static void wwan_hwsim_dev_del_work(struct work_struct *work);
|
||
|
|
||
|
static netdev_tx_t wwan_hwsim_netdev_xmit(struct sk_buff *skb,
|
||
|
struct net_device *ndev)
|
||
|
{
|
||
|
ndev->stats.tx_packets++;
|
||
|
ndev->stats.tx_bytes += skb->len;
|
||
|
consume_skb(skb);
|
||
|
return NETDEV_TX_OK;
|
||
|
}
|
||
|
|
||
|
static const struct net_device_ops wwan_hwsim_netdev_ops = {
|
||
|
.ndo_start_xmit = wwan_hwsim_netdev_xmit,
|
||
|
};
|
||
|
|
||
|
static void wwan_hwsim_netdev_setup(struct net_device *ndev)
|
||
|
{
|
||
|
ndev->netdev_ops = &wwan_hwsim_netdev_ops;
|
||
|
ndev->needs_free_netdev = true;
|
||
|
|
||
|
ndev->mtu = ETH_DATA_LEN;
|
||
|
ndev->min_mtu = ETH_MIN_MTU;
|
||
|
ndev->max_mtu = ETH_MAX_MTU;
|
||
|
|
||
|
ndev->type = ARPHRD_NONE;
|
||
|
ndev->flags = IFF_POINTOPOINT | IFF_NOARP;
|
||
|
}
|
||
|
|
||
|
static const struct wwan_ops wwan_hwsim_wwan_rtnl_ops = {
|
||
|
.priv_size = 0, /* No private data */
|
||
|
.setup = wwan_hwsim_netdev_setup,
|
||
|
};
|
||
|
|
||
|
static int wwan_hwsim_port_start(struct wwan_port *wport)
|
||
|
{
|
||
|
struct wwan_hwsim_port *port = wwan_port_get_drvdata(wport);
|
||
|
|
||
|
port->pstate = AT_PARSER_WAIT_A;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static void wwan_hwsim_port_stop(struct wwan_port *wport)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
/* Implements a minimalistic AT commands parser that echo input back and
|
||
|
* reply with 'OK' to each input command. See AT command protocol details in the
|
||
|
* ITU-T V.250 recomendations document.
|
||
|
*
|
||
|
* Be aware that this processor is not fully V.250 compliant.
|
||
|
*/
|
||
|
static int wwan_hwsim_port_tx(struct wwan_port *wport, struct sk_buff *in)
|
||
|
{
|
||
|
struct wwan_hwsim_port *port = wwan_port_get_drvdata(wport);
|
||
|
struct sk_buff *out;
|
||
|
int i, n, s;
|
||
|
|
||
|
/* Estimate a max possible number of commands by counting the number of
|
||
|
* termination chars (S3 param, CR by default). And then allocate the
|
||
|
* output buffer that will be enough to fit the echo and result codes of
|
||
|
* all commands.
|
||
|
*/
|
||
|
for (i = 0, n = 0; i < in->len; ++i)
|
||
|
if (in->data[i] == '\r')
|
||
|
n++;
|
||
|
n = in->len + n * (2 + 2 + 2); /* Output buffer size */
|
||
|
out = alloc_skb(n, GFP_KERNEL);
|
||
|
if (!out)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
for (i = 0, s = 0; i < in->len; ++i) {
|
||
|
char c = in->data[i];
|
||
|
|
||
|
if (port->pstate == AT_PARSER_WAIT_A) {
|
||
|
if (c == 'A' || c == 'a')
|
||
|
port->pstate = AT_PARSER_WAIT_T;
|
||
|
else if (c != '\n') /* Ignore formating char */
|
||
|
port->pstate = AT_PARSER_SKIP_LINE;
|
||
|
} else if (port->pstate == AT_PARSER_WAIT_T) {
|
||
|
if (c == 'T' || c == 't')
|
||
|
port->pstate = AT_PARSER_WAIT_TERM;
|
||
|
else
|
||
|
port->pstate = AT_PARSER_SKIP_LINE;
|
||
|
} else if (port->pstate == AT_PARSER_WAIT_TERM) {
|
||
|
if (c != '\r')
|
||
|
continue;
|
||
|
/* Consume the trailing formatting char as well */
|
||
|
if ((i + 1) < in->len && in->data[i + 1] == '\n')
|
||
|
i++;
|
||
|
n = i - s + 1;
|
||
|
memcpy(skb_put(out, n), &in->data[s], n);/* Echo */
|
||
|
memcpy(skb_put(out, 6), "\r\nOK\r\n", 6);
|
||
|
s = i + 1;
|
||
|
port->pstate = AT_PARSER_WAIT_A;
|
||
|
} else if (port->pstate == AT_PARSER_SKIP_LINE) {
|
||
|
if (c != '\r')
|
||
|
continue;
|
||
|
port->pstate = AT_PARSER_WAIT_A;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (i > s) {
|
||
|
/* Echo the processed portion of a not yet completed command */
|
||
|
n = i - s;
|
||
|
memcpy(skb_put(out, n), &in->data[s], n);
|
||
|
}
|
||
|
|
||
|
consume_skb(in);
|
||
|
|
||
|
wwan_port_rx(wport, out);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static const struct wwan_port_ops wwan_hwsim_port_ops = {
|
||
|
.start = wwan_hwsim_port_start,
|
||
|
.stop = wwan_hwsim_port_stop,
|
||
|
.tx = wwan_hwsim_port_tx,
|
||
|
};
|
||
|
|
||
|
static struct wwan_hwsim_port *wwan_hwsim_port_new(struct wwan_hwsim_dev *dev)
|
||
|
{
|
||
|
struct wwan_hwsim_port *port;
|
||
|
char name[0x10];
|
||
|
int err;
|
||
|
|
||
|
port = kzalloc(sizeof(*port), GFP_KERNEL);
|
||
|
if (!port)
|
||
|
return ERR_PTR(-ENOMEM);
|
||
|
|
||
|
port->dev = dev;
|
||
|
|
||
|
spin_lock(&dev->ports_lock);
|
||
|
port->id = dev->port_idx++;
|
||
|
spin_unlock(&dev->ports_lock);
|
||
|
|
||
|
port->wwan = wwan_create_port(&dev->dev, WWAN_PORT_AT,
|
||
|
&wwan_hwsim_port_ops,
|
||
|
port);
|
||
|
if (IS_ERR(port->wwan)) {
|
||
|
err = PTR_ERR(port->wwan);
|
||
|
goto err_free_port;
|
||
|
}
|
||
|
|
||
|
INIT_WORK(&port->del_work, wwan_hwsim_port_del_work);
|
||
|
|
||
|
snprintf(name, sizeof(name), "port%u", port->id);
|
||
|
port->debugfs_topdir = debugfs_create_dir(name, dev->debugfs_topdir);
|
||
|
debugfs_create_file("destroy", 0200, port->debugfs_topdir, port,
|
||
|
&wwan_hwsim_debugfs_portdestroy_fops);
|
||
|
|
||
|
return port;
|
||
|
|
||
|
err_free_port:
|
||
|
kfree(port);
|
||
|
|
||
|
return ERR_PTR(err);
|
||
|
}
|
||
|
|
||
|
static void wwan_hwsim_port_del(struct wwan_hwsim_port *port)
|
||
|
{
|
||
|
debugfs_remove(port->debugfs_topdir);
|
||
|
|
||
|
/* Make sure that there is no pending deletion work */
|
||
|
if (current_work() != &port->del_work)
|
||
|
cancel_work_sync(&port->del_work);
|
||
|
|
||
|
wwan_remove_port(port->wwan);
|
||
|
kfree(port);
|
||
|
}
|
||
|
|
||
|
static void wwan_hwsim_port_del_work(struct work_struct *work)
|
||
|
{
|
||
|
struct wwan_hwsim_port *port =
|
||
|
container_of(work, typeof(*port), del_work);
|
||
|
struct wwan_hwsim_dev *dev = port->dev;
|
||
|
|
||
|
spin_lock(&dev->ports_lock);
|
||
|
if (list_empty(&port->list)) {
|
||
|
/* Someone else deleting port at the moment */
|
||
|
spin_unlock(&dev->ports_lock);
|
||
|
return;
|
||
|
}
|
||
|
list_del_init(&port->list);
|
||
|
spin_unlock(&dev->ports_lock);
|
||
|
|
||
|
wwan_hwsim_port_del(port);
|
||
|
}
|
||
|
|
||
|
static void wwan_hwsim_dev_release(struct device *sysdev)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev = container_of(sysdev, typeof(*dev), dev);
|
||
|
|
||
|
kfree(dev);
|
||
|
}
|
||
|
|
||
|
static struct wwan_hwsim_dev *wwan_hwsim_dev_new(void)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev;
|
||
|
int err;
|
||
|
|
||
|
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
|
||
|
if (!dev)
|
||
|
return ERR_PTR(-ENOMEM);
|
||
|
|
||
|
spin_lock(&wwan_hwsim_devs_lock);
|
||
|
dev->id = wwan_hwsim_dev_idx++;
|
||
|
spin_unlock(&wwan_hwsim_devs_lock);
|
||
|
|
||
|
dev->dev.release = wwan_hwsim_dev_release;
|
||
|
dev->dev.class = wwan_hwsim_class;
|
||
|
dev_set_name(&dev->dev, "hwsim%u", dev->id);
|
||
|
|
||
|
spin_lock_init(&dev->ports_lock);
|
||
|
INIT_LIST_HEAD(&dev->ports);
|
||
|
|
||
|
err = device_register(&dev->dev);
|
||
|
if (err)
|
||
|
goto err_free_dev;
|
||
|
|
||
|
INIT_WORK(&dev->del_work, wwan_hwsim_dev_del_work);
|
||
|
|
||
|
err = wwan_register_ops(&dev->dev, &wwan_hwsim_wwan_rtnl_ops, dev, 1);
|
||
|
if (err)
|
||
|
goto err_unreg_dev;
|
||
|
|
||
|
dev->debugfs_topdir = debugfs_create_dir(dev_name(&dev->dev),
|
||
|
wwan_hwsim_debugfs_topdir);
|
||
|
debugfs_create_file("destroy", 0200, dev->debugfs_topdir, dev,
|
||
|
&wwan_hwsim_debugfs_devdestroy_fops);
|
||
|
dev->debugfs_portcreate =
|
||
|
debugfs_create_file("portcreate", 0200,
|
||
|
dev->debugfs_topdir, dev,
|
||
|
&wwan_hwsim_debugfs_portcreate_fops);
|
||
|
|
||
|
return dev;
|
||
|
|
||
|
err_unreg_dev:
|
||
|
device_unregister(&dev->dev);
|
||
|
/* Memory will be freed in the device release callback */
|
||
|
|
||
|
return ERR_PTR(err);
|
||
|
|
||
|
err_free_dev:
|
||
|
kfree(dev);
|
||
|
|
||
|
return ERR_PTR(err);
|
||
|
}
|
||
|
|
||
|
static void wwan_hwsim_dev_del(struct wwan_hwsim_dev *dev)
|
||
|
{
|
||
|
debugfs_remove(dev->debugfs_portcreate); /* Avoid new ports */
|
||
|
|
||
|
spin_lock(&dev->ports_lock);
|
||
|
while (!list_empty(&dev->ports)) {
|
||
|
struct wwan_hwsim_port *port;
|
||
|
|
||
|
port = list_first_entry(&dev->ports, struct wwan_hwsim_port,
|
||
|
list);
|
||
|
list_del_init(&port->list);
|
||
|
spin_unlock(&dev->ports_lock);
|
||
|
wwan_hwsim_port_del(port);
|
||
|
spin_lock(&dev->ports_lock);
|
||
|
}
|
||
|
spin_unlock(&dev->ports_lock);
|
||
|
|
||
|
debugfs_remove(dev->debugfs_topdir);
|
||
|
|
||
|
/* This will remove all child netdev(s) */
|
||
|
wwan_unregister_ops(&dev->dev);
|
||
|
|
||
|
/* Make sure that there is no pending deletion work */
|
||
|
if (current_work() != &dev->del_work)
|
||
|
cancel_work_sync(&dev->del_work);
|
||
|
|
||
|
device_unregister(&dev->dev);
|
||
|
/* Memory will be freed in the device release callback */
|
||
|
}
|
||
|
|
||
|
static void wwan_hwsim_dev_del_work(struct work_struct *work)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev = container_of(work, typeof(*dev), del_work);
|
||
|
|
||
|
spin_lock(&wwan_hwsim_devs_lock);
|
||
|
if (list_empty(&dev->list)) {
|
||
|
/* Someone else deleting device at the moment */
|
||
|
spin_unlock(&wwan_hwsim_devs_lock);
|
||
|
return;
|
||
|
}
|
||
|
list_del_init(&dev->list);
|
||
|
spin_unlock(&wwan_hwsim_devs_lock);
|
||
|
|
||
|
wwan_hwsim_dev_del(dev);
|
||
|
}
|
||
|
|
||
|
static ssize_t wwan_hwsim_debugfs_portdestroy_write(struct file *file,
|
||
|
const char __user *usrbuf,
|
||
|
size_t count, loff_t *ppos)
|
||
|
{
|
||
|
struct wwan_hwsim_port *port = file->private_data;
|
||
|
|
||
|
/* We can not delete port here since it will cause a deadlock due to
|
||
|
* waiting this callback to finish in the debugfs_remove() call. So,
|
||
|
* use workqueue.
|
||
|
*/
|
||
|
schedule_work(&port->del_work);
|
||
|
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
static const struct file_operations wwan_hwsim_debugfs_portdestroy_fops = {
|
||
|
.write = wwan_hwsim_debugfs_portdestroy_write,
|
||
|
.open = simple_open,
|
||
|
.llseek = noop_llseek,
|
||
|
};
|
||
|
|
||
|
static ssize_t wwan_hwsim_debugfs_portcreate_write(struct file *file,
|
||
|
const char __user *usrbuf,
|
||
|
size_t count, loff_t *ppos)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev = file->private_data;
|
||
|
struct wwan_hwsim_port *port;
|
||
|
|
||
|
port = wwan_hwsim_port_new(dev);
|
||
|
if (IS_ERR(port))
|
||
|
return PTR_ERR(port);
|
||
|
|
||
|
spin_lock(&dev->ports_lock);
|
||
|
list_add_tail(&port->list, &dev->ports);
|
||
|
spin_unlock(&dev->ports_lock);
|
||
|
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
static const struct file_operations wwan_hwsim_debugfs_portcreate_fops = {
|
||
|
.write = wwan_hwsim_debugfs_portcreate_write,
|
||
|
.open = simple_open,
|
||
|
.llseek = noop_llseek,
|
||
|
};
|
||
|
|
||
|
static ssize_t wwan_hwsim_debugfs_devdestroy_write(struct file *file,
|
||
|
const char __user *usrbuf,
|
||
|
size_t count, loff_t *ppos)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev = file->private_data;
|
||
|
|
||
|
/* We can not delete device here since it will cause a deadlock due to
|
||
|
* waiting this callback to finish in the debugfs_remove() call. So,
|
||
|
* use workqueue.
|
||
|
*/
|
||
|
schedule_work(&dev->del_work);
|
||
|
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
static const struct file_operations wwan_hwsim_debugfs_devdestroy_fops = {
|
||
|
.write = wwan_hwsim_debugfs_devdestroy_write,
|
||
|
.open = simple_open,
|
||
|
.llseek = noop_llseek,
|
||
|
};
|
||
|
|
||
|
static ssize_t wwan_hwsim_debugfs_devcreate_write(struct file *file,
|
||
|
const char __user *usrbuf,
|
||
|
size_t count, loff_t *ppos)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev;
|
||
|
|
||
|
dev = wwan_hwsim_dev_new();
|
||
|
if (IS_ERR(dev))
|
||
|
return PTR_ERR(dev);
|
||
|
|
||
|
spin_lock(&wwan_hwsim_devs_lock);
|
||
|
list_add_tail(&dev->list, &wwan_hwsim_devs);
|
||
|
spin_unlock(&wwan_hwsim_devs_lock);
|
||
|
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
static const struct file_operations wwan_hwsim_debugfs_devcreate_fops = {
|
||
|
.write = wwan_hwsim_debugfs_devcreate_write,
|
||
|
.open = simple_open,
|
||
|
.llseek = noop_llseek,
|
||
|
};
|
||
|
|
||
|
static int __init wwan_hwsim_init_devs(void)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev;
|
||
|
int i, j;
|
||
|
|
||
|
for (i = 0; i < wwan_hwsim_devsnum; ++i) {
|
||
|
dev = wwan_hwsim_dev_new();
|
||
|
if (IS_ERR(dev))
|
||
|
return PTR_ERR(dev);
|
||
|
|
||
|
spin_lock(&wwan_hwsim_devs_lock);
|
||
|
list_add_tail(&dev->list, &wwan_hwsim_devs);
|
||
|
spin_unlock(&wwan_hwsim_devs_lock);
|
||
|
|
||
|
/* Create a couple of ports per each device to accelerate
|
||
|
* the simulator readiness time.
|
||
|
*/
|
||
|
for (j = 0; j < 2; ++j) {
|
||
|
struct wwan_hwsim_port *port;
|
||
|
|
||
|
port = wwan_hwsim_port_new(dev);
|
||
|
if (IS_ERR(port))
|
||
|
return PTR_ERR(port);
|
||
|
|
||
|
spin_lock(&dev->ports_lock);
|
||
|
list_add_tail(&port->list, &dev->ports);
|
||
|
spin_unlock(&dev->ports_lock);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static void wwan_hwsim_free_devs(void)
|
||
|
{
|
||
|
struct wwan_hwsim_dev *dev;
|
||
|
|
||
|
spin_lock(&wwan_hwsim_devs_lock);
|
||
|
while (!list_empty(&wwan_hwsim_devs)) {
|
||
|
dev = list_first_entry(&wwan_hwsim_devs, struct wwan_hwsim_dev,
|
||
|
list);
|
||
|
list_del_init(&dev->list);
|
||
|
spin_unlock(&wwan_hwsim_devs_lock);
|
||
|
wwan_hwsim_dev_del(dev);
|
||
|
spin_lock(&wwan_hwsim_devs_lock);
|
||
|
}
|
||
|
spin_unlock(&wwan_hwsim_devs_lock);
|
||
|
}
|
||
|
|
||
|
static int __init wwan_hwsim_init(void)
|
||
|
{
|
||
|
int err;
|
||
|
|
||
|
if (wwan_hwsim_devsnum < 0 || wwan_hwsim_devsnum > 128)
|
||
|
return -EINVAL;
|
||
|
|
||
|
wwan_hwsim_class = class_create(THIS_MODULE, "wwan_hwsim");
|
||
|
if (IS_ERR(wwan_hwsim_class))
|
||
|
return PTR_ERR(wwan_hwsim_class);
|
||
|
|
||
|
wwan_hwsim_debugfs_topdir = debugfs_create_dir("wwan_hwsim", NULL);
|
||
|
wwan_hwsim_debugfs_devcreate =
|
||
|
debugfs_create_file("devcreate", 0200,
|
||
|
wwan_hwsim_debugfs_topdir, NULL,
|
||
|
&wwan_hwsim_debugfs_devcreate_fops);
|
||
|
|
||
|
err = wwan_hwsim_init_devs();
|
||
|
if (err)
|
||
|
goto err_clean_devs;
|
||
|
|
||
|
return 0;
|
||
|
|
||
|
err_clean_devs:
|
||
|
wwan_hwsim_free_devs();
|
||
|
debugfs_remove(wwan_hwsim_debugfs_topdir);
|
||
|
class_destroy(wwan_hwsim_class);
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
static void __exit wwan_hwsim_exit(void)
|
||
|
{
|
||
|
debugfs_remove(wwan_hwsim_debugfs_devcreate); /* Avoid new devs */
|
||
|
wwan_hwsim_free_devs();
|
||
|
flush_scheduled_work(); /* Wait deletion works completion */
|
||
|
debugfs_remove(wwan_hwsim_debugfs_topdir);
|
||
|
class_destroy(wwan_hwsim_class);
|
||
|
}
|
||
|
|
||
|
module_init(wwan_hwsim_init);
|
||
|
module_exit(wwan_hwsim_exit);
|
||
|
|
||
|
MODULE_AUTHOR("Sergey Ryazanov");
|
||
|
MODULE_DESCRIPTION("Device simulator for WWAN framework");
|
||
|
MODULE_LICENSE("GPL");
|