#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/time.h>
#include <linux/list.h>
#include <linux/skbuff.h>
#include <linux/timer.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 const struct class wwan_hwsim_class = {
.name = "wwan_hwsim",
};
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;
static struct workqueue_struct *wwan_wq;
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;
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;
union {
struct {
enum {
AT_PARSER_WAIT_A,
AT_PARSER_WAIT_T,
AT_PARSER_WAIT_TERM,
AT_PARSER_SKIP_LINE,
} pstate;
} at_emul;
struct {
struct timer_list timer;
} nmea_emul;
};
};
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,
.setup = wwan_hwsim_netdev_setup,
};
static int wwan_hwsim_at_emul_start(struct wwan_port *wport)
{
struct wwan_hwsim_port *port = wwan_port_get_drvdata(wport);
port->at_emul.pstate = AT_PARSER_WAIT_A;
return 0;
}
static void wwan_hwsim_at_emul_stop(struct wwan_port *wport)
{
}
static int wwan_hwsim_at_emul_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;
for (i = 0, n = 0; i < in->len; ++i)
if (in->data[i] == '\r')
n++;
n = in->len + n * (2 + 2 + 2);
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->at_emul.pstate == AT_PARSER_WAIT_A) {
if (c == 'A' || c == 'a')
port->at_emul.pstate = AT_PARSER_WAIT_T;
else if (c != '\n')
port->at_emul.pstate = AT_PARSER_SKIP_LINE;
} else if (port->at_emul.pstate == AT_PARSER_WAIT_T) {
if (c == 'T' || c == 't')
port->at_emul.pstate = AT_PARSER_WAIT_TERM;
else
port->at_emul.pstate = AT_PARSER_SKIP_LINE;
} else if (port->at_emul.pstate == AT_PARSER_WAIT_TERM) {
if (c != '\r')
continue;
if ((i + 1) < in->len && in->data[i + 1] == '\n')
i++;
n = i - s + 1;
skb_put_data(out, &in->data[s], n);
skb_put_data(out, "\r\nOK\r\n", 6);
s = i + 1;
port->at_emul.pstate = AT_PARSER_WAIT_A;
} else if (port->at_emul.pstate == AT_PARSER_SKIP_LINE) {
if (c != '\r')
continue;
port->at_emul.pstate = AT_PARSER_WAIT_A;
}
}
if (i > s) {
n = i - s;
skb_put_data(out, &in->data[s], n);
}
consume_skb(in);
wwan_port_rx(wport, out);
return 0;
}
static const struct wwan_port_ops wwan_hwsim_at_emul_port_ops = {
.start = wwan_hwsim_at_emul_start,
.stop = wwan_hwsim_at_emul_stop,
.tx = wwan_hwsim_at_emul_tx,
};
#if IS_ENABLED(CONFIG_GNSS)
#define NMEA_MAX_LEN 82
#define NMEA_TRAIL_LEN 5
#define NMEA_MAX_DATA_LEN (NMEA_MAX_LEN - NMEA_TRAIL_LEN)
static __printf(2, 3)
void wwan_hwsim_nmea_skb_push_sentence(struct sk_buff *skb,
const char *fmt, ...)
{
unsigned char *s, *p;
va_list ap;
u8 cs = 0;
int len;
s = skb_put(skb, NMEA_MAX_LEN + 1);
if (!s)
return;
va_start(ap, fmt);
len = vsnprintf(s, NMEA_MAX_DATA_LEN + 1, fmt, ap);
va_end(ap);
if (WARN_ON_ONCE(len > NMEA_MAX_DATA_LEN))
return;
for (p = s + 1; *p != '\0'; ++p)
cs ^= *p;
p += snprintf(p, 5 + 1, "*%02X\r\n", cs);
len = (p - s) - (NMEA_MAX_LEN + 1);
skb->tail += len;
skb->len += len;
}
static void wwan_hwsim_nmea_emul_timer(struct timer_list *t)
{
static const unsigned int coord[4 * 2] = { 43, 44, 8528, 0,
11, 15, 4559, 0 };
struct wwan_hwsim_port *port = timer_container_of(port, t, nmea_emul.timer);
struct sk_buff *skb;
struct tm tm;
time64_to_tm(ktime_get_real_seconds(), 0, &tm);
mod_timer(&port->nmea_emul.timer, jiffies + HZ);
skb = alloc_skb(NMEA_MAX_LEN * 2 + 2, GFP_ATOMIC);
if (!skb)
return;
wwan_hwsim_nmea_skb_push_sentence(skb,
"$GPGGA,%02u%02u%02u.000,%02u%02u.%04u,%c,%03u%02u.%04u,%c,1,7,1.03,176.2,M,55.2,M,,",
tm.tm_hour, tm.tm_min, tm.tm_sec,
coord[0], coord[1], coord[2],
coord[3] ? 'S' : 'N',
coord[4], coord[5], coord[6],
coord[7] ? 'W' : 'E');
wwan_hwsim_nmea_skb_push_sentence(skb,
"$GPRMC,%02u%02u%02u.000,A,%02u%02u.%04u,%c,%03u%02u.%04u,%c,0.02,31.66,%02u%02u%02u,,,A",
tm.tm_hour, tm.tm_min, tm.tm_sec,
coord[0], coord[1], coord[2],
coord[3] ? 'S' : 'N',
coord[4], coord[5], coord[6],
coord[7] ? 'W' : 'E',
tm.tm_mday, tm.tm_mon + 1,
(unsigned int)tm.tm_year - 100);
wwan_port_rx(port->wwan, skb);
}
static int wwan_hwsim_nmea_emul_start(struct wwan_port *wport)
{
struct wwan_hwsim_port *port = wwan_port_get_drvdata(wport);
timer_setup(&port->nmea_emul.timer, wwan_hwsim_nmea_emul_timer, 0);
wwan_hwsim_nmea_emul_timer(&port->nmea_emul.timer);
return 0;
}
static void wwan_hwsim_nmea_emul_stop(struct wwan_port *wport)
{
struct wwan_hwsim_port *port = wwan_port_get_drvdata(wport);
timer_delete_sync(&port->nmea_emul.timer);
}
static int wwan_hwsim_nmea_emul_tx(struct wwan_port *wport, struct sk_buff *in)
{
consume_skb(in);
return 0;
}
static const struct wwan_port_ops wwan_hwsim_nmea_emul_port_ops = {
.start = wwan_hwsim_nmea_emul_start,
.stop = wwan_hwsim_nmea_emul_stop,
.tx = wwan_hwsim_nmea_emul_tx,
};
#endif
static struct wwan_hwsim_port *wwan_hwsim_port_new(struct wwan_hwsim_dev *dev,
enum wwan_port_type type)
{
const struct wwan_port_ops *ops;
struct wwan_hwsim_port *port;
char name[0x10];
int err;
if (type == WWAN_PORT_AT)
ops = &wwan_hwsim_at_emul_port_ops;
#if IS_ENABLED(CONFIG_GNSS)
else if (type == WWAN_PORT_NMEA)
ops = &wwan_hwsim_nmea_emul_port_ops;
#endif
else
return ERR_PTR(-EINVAL);
port = kzalloc_obj(*port);
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, type, ops, NULL, 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);
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)) {
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_obj(*dev);
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);
return ERR_PTR(err);
err_free_dev:
put_device(&dev->dev);
return ERR_PTR(err);
}
static void wwan_hwsim_dev_del(struct wwan_hwsim_dev *dev)
{
debugfs_remove(dev->debugfs_portcreate);
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);
wwan_unregister_ops(&dev->dev);
if (current_work() != &dev->del_work)
cancel_work_sync(&dev->del_work);
device_unregister(&dev->dev);
}
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)) {
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;
queue_work(wwan_wq, &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, WWAN_PORT_AT);
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;
queue_work(wwan_wq, &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) {
struct wwan_hwsim_port *port;
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);
for (j = 0; j < 2; ++j) {
port = wwan_hwsim_port_new(dev, WWAN_PORT_AT);
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);
}
#if IS_ENABLED(CONFIG_GNSS)
port = wwan_hwsim_port_new(dev, WWAN_PORT_NMEA);
if (IS_ERR(port)) {
dev_warn(&dev->dev, "failed to create initial NMEA port: %d\n",
(int)PTR_ERR(port));
} else {
spin_lock(&dev->ports_lock);
list_add_tail(&port->list, &dev->ports);
spin_unlock(&dev->ports_lock);
}
#endif
}
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_wq = alloc_workqueue("wwan_wq", WQ_PERCPU, 0);
if (!wwan_wq)
return -ENOMEM;
err = class_register(&wwan_hwsim_class);
if (err)
goto err_wq_destroy;
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:
debugfs_remove(wwan_hwsim_debugfs_devcreate);
wwan_hwsim_free_devs();
flush_workqueue(wwan_wq);
debugfs_remove(wwan_hwsim_debugfs_topdir);
class_unregister(&wwan_hwsim_class);
err_wq_destroy:
destroy_workqueue(wwan_wq);
return err;
}
static void __exit wwan_hwsim_exit(void)
{
debugfs_remove(wwan_hwsim_debugfs_devcreate);
wwan_hwsim_free_devs();
flush_workqueue(wwan_wq);
debugfs_remove(wwan_hwsim_debugfs_topdir);
class_unregister(&wwan_hwsim_class);
destroy_workqueue(wwan_wq);
}
module_init(wwan_hwsim_init);
module_exit(wwan_hwsim_exit);
MODULE_AUTHOR("Sergey Ryazanov");
MODULE_DESCRIPTION("Device simulator for WWAN framework");
MODULE_LICENSE("GPL");