Re: Huawei E398 cdc/serialmodem-ppp 3G/4G

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Thomas Schäfer <tschaefer@xxxxxxxxxxx> writes:

> in December 2011 here was a short discussion about the cdc/ndis-mode for 
> surfstick Huawei 398 using as "network card" with dhcp, not as modem with 
> pppd.
>
>
> I am a user of this stick. I would be very happy for some more information 
> about that.
>
> At the moment I use this stick under linux with following steps:
>
> usb_modeswitch -v 12d1 -p 1505 -M 
> "55534243123456780000000000000011062000000100000000000000000000"
>
> after that "option" loads
>
> then
>
> dialling and ppp via /dev/USB0
>
> This works for me as long there is no LTE-tower within reach.
>
> Is the stick in LTE/4G-Mode my old scripts do not work.
> I found out, that I can force the stick to 3G with
>
> at+cops=0,0,"Telekom.de",2
>
>
> (the last number: 2 >> 3G, 7 >>4G)
>
> But this is not a fine solution.
>
> Unfortunately I am not a big programmer. I can only apply simple scripts and 
> patches and provide results of tests.

I was hoping to quickly have something usable for this list, but it
seems that I don't have much as spare time as I would like, and I am
using far too much time debugging problems which are most likely obvious
to anyone with any Linux driver experience...

Anyway, if you are daring you might try the attached patch.  It is ugly
as hell, has lots and lots of useless debug messages and it does random
locking at best. It's also provided as a big patch blob against 3.1.x
with an embedded cdc_ether.c change as well, since I did not have time
to fix a proper git history (the real history is only amusing if you've
never seen a newbie struggle with a Linux driver before :-)


Status is: "Works for me. Will most likely crash at some point in
time." 

But it doesn't usually blow up. And the network interface works without
problems since this is basically just a wrapper around cdc_ether.

I choose to post this to the linux-usb list now to get some early
feedback, although it is nowhere near ready for any submission.
Encouraged by the very good feedback I got from Dan Williams
previously.  That's what you get for helping people.  Please let me know
if I'm crossing the line.

And if I'm lucky, maybe also get some helping hand from someone with
more experience and/or more spare time?


A few notes on the current design:
- cdc_enc is a new "subdriver" which will expose the CDC encapsulated
  commands as a character device.  As this could be interesting for
  multiple drivers, and not only QMI based devices, I have chosen to
  create a separate module for this
- qmi_wwan is a simple usbnet minidriver based on cdc_ether, using the
  cdc_enc driver to expose a QMI character device.  It also includes the
  bare minimum of QMI commands for starting networking, with the
  exception of PIN code verification (as that cannot be done without
  configuration).

There are quite a few things I'm not satisfied with myself, like the
per-client buffers and looking into the packets to forward them the the
correct client.  All that could be simplified a lot by just accepting
that all clients receive all data, and leave any packet filtering to
userspace.  It's not like the QMI traffic ever is going to be high...

That would also make the character device completely protocol
agnostic. Like it is now, cdc_enc doesn't know anything about QMI, but
the registering driver (qmi_wwan) provides pointers to QMI specific
multiplexing helpers.  That's pretty pointless.

The receive strategy is also somewhat weird.  The interrupt endpoint is
owned by the networking driver, but we link into that to enable us to
act immediately on notifications.  However, this does of course not work
when networking is down.  So we submit the urb whenever we're wanting
data from the device.  Often from multiple clients at the same time.

I would appreciate any comments on this.  Dan Williams has already given
very useful feedback on my previous attempts.  Thanks! Most of the QMI
protocol code is now gone (but there is more to go as noted above). And
using a character device makes things so much easier. I like being able
to do debug dumps like this (this is actual QMI data sent to and from my
modem, while establishing the connection):

nemi:/lib/modules/3.1.0-1-amd64/misc# hexdump -C </dev/cdc-enc0 
00000000  01 0b 00 80 00 00 02 00  27 00 00 00 01 0b 00 80  |........'.......|
00000010  00 00 02 00 27 00 00 00  01 0b 00 80 00 00 02 00  |....'...........|
00000020  27 00 00 00 01 0b 00 80  00 00 02 00 27 00 00 00  |'...........'...|
00000030  01 0b 00 80 00 00 02 00  27 00 00 00 01 0b 00 80  |........'.......|
00000040  00 00 02 00 27 00 00 00  01 0f 00 00 00 00 00 01  |....'...........|
00000050  22 00 04 00 01 01 00 01  01 17 00 80 00 00 01 01  |"...............|
00000060  22 00 0c 00 02 04 00 00  00 00 00 01 02 00 01 01  |"...............|
00000070  01 17 00 80 01 01 02 02  00 22 00 0b 00 02 04 00  |........."......|
00000080  00 00 00 00 01 01 00 01  01 1a 00 80 01 01 02 03  |................|
00000090  00 20 00 0e 00 02 04 00  00 00 00 00 01 04 00 e8  |. ..............|
000000a0  90 15 02 01 15 00 80 01  ff 04 00 00 22 00 09 00  |............"...|
000000b0  01 02 00 02 00 12 01 00  04 01 0b 00 80 00 00 02  |................|
000000c0  00 27 00 00 00 01 0b 00  80 00 00 02 00 27 00 00  |.'...........'..|


But there are so many other questions...  Device and class naming?  Use
a dedicated class at all, or maybe register under "usb" like cdc-wdm?
Power Management - magic?  

And please let me have the "That's stupid!  Why don't you just do..."
comments. I am desperately trying to learn this coding thing, but I
don't need to experience every ooops/panic/BUG myself... Although it
feels like I already have :-)


So, how to use the driver?  It creates two devices:
1) the usual wwanX network interface:

 nemi:/tmp# ifconfig wwan1
 wwan1     Link encap:Ethernet  HWaddr 00:a0:c6:00:00:00  
           inet6 addr: fe80::2a0:c6ff:fe00:0/64 Scope:Link
           UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
           RX packets:293 errors:0 dropped:0 overruns:0 frame:0
           TX packets:299 errors:0 dropped:0 overruns:0 carrier:0
           collisions:0 txqueuelen:1000 
           RX bytes:81428 (79.5 KiB)  TX bytes:100374 (98.0 KiB)


2) and a new /dev/cdc-encX character device:

 nemi:/tmp# ls -l /dev/cdc-enc0 
 crw------- 1 root root 251, 0 Jan 12 20:56 /dev/cdc-enc0


You can safely ignore the latter if you don't want to play with QMI
commands.  It's just there in case you *do* want to send or receive QMI.
Connecting is (provided that the stored profile on the card allows this)
as easy as:

a) enter PIN code using e.g. one of the "option" ttyUSBx devices:
     echo AT+CPIN="xxxx" >/dev/ttyUSB0

b) run your favourite dhcp client to bring up the link and configure
  addresses: 

 nemi:/tmp# dhclient -v -pf /var/run/dhclient.wwan1.pid -lf /var/lib/dhcp/dhclient.wwan1.leases wwan1
 Internet Systems Consortium DHCP Client 4.1.1-P1
 Copyright 2004-2010 Internet Systems Consortium.
 All rights reserved.
 For info, please visit https://www.isc.org/software/dhcp/

 Listening on LPF/wwan1/00:a0:c6:00:00:00
 Sending on   LPF/wwan1/00:a0:c6:00:00:00
 Sending on   Socket/fallback
 DHCPREQUEST on wwan1 to 255.255.255.255 port 67
 DHCPACK from 77.18.149.190
 bound to 77.18.149.189 -- renewal in 2996 seconds.


And you hopefully got something like this:

nemi:/tmp# ifconfig wwan1
wwan1     Link encap:Ethernet  HWaddr 00:a0:c6:00:00:00  
          inet addr:77.18.149.189  Bcast:77.18.149.191  Mask:255.255.255.252
          inet6 addr: fe80::2a0:c6ff:fe00:0/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:533 errors:0 dropped:0 overruns:0 frame:0
          TX packets:539 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:150052 (146.5 KiB)  TX bytes:182454 (178.1 KiB)




Notes regarding the cdc-encX device:

It will be rooted under the same USB device as the Ethernet device.  I.e
the CDC Ethernet control USB interface.  The cdc-enc0 device doesn't
really have any actual parent device, as it only uses control messages
and is piggy-backed on another driver.  But putting it under that
interface still makes sense IMHO, and it makes it really easy to map
between the two:

nemi:/tmp# ls -l /sys/class/net/wwan1/device
lrwxrwxrwx 1 root root 0 Jan 12 20:58 /sys/class/net/wwan1/device -> ../../../2-2:1.3
nemi:/tmp# ls -l /sys/class/net/wwan1/device/
total 0
-r--r--r-- 1 root root 4096 Jan 12 22:04 bAlternateSetting
-r--r--r-- 1 root root 4096 Jan 12 20:56 bInterfaceClass
-r--r--r-- 1 root root 4096 Jan 12 20:56 bInterfaceNumber
-r--r--r-- 1 root root 4096 Jan 12 22:04 bInterfaceProtocol
-r--r--r-- 1 root root 4096 Jan 12 22:04 bInterfaceSubClass
-r--r--r-- 1 root root 4096 Jan 12 22:04 bNumEndpoints
drwxr-xr-x 3 root root    0 Jan 12 20:56 cdc_enc
lrwxrwxrwx 1 root root    0 Jan 12 20:56 driver -> ../../../../../../bus/usb/drivers/qmi_wwan
drwxr-xr-x 3 root root    0 Jan 12 22:04 ep_85
-r--r--r-- 1 root root 4096 Jan 12 22:04 modalias
drwxr-xr-x 3 root root    0 Jan 12 20:56 net
drwxr-xr-x 2 root root    0 Jan 12 20:58 power
lrwxrwxrwx 1 root root    0 Jan 12 20:56 subsystem -> ../../../../../../bus/usb
-r--r--r-- 1 root root 4096 Jan 12 22:04 supports_autosuspend
-rw-r--r-- 1 root root 4096 Jan 12 20:56 uevent
nemi:/tmp# ls -l /sys/class/net/wwan1/device/cdc_enc/
total 0
drwxr-xr-x 3 root root 0 Jan 12 20:56 cdc-enc0
nemi:/tmp# ls -l /sys/class/net/wwan1/device/cdc_enc/cdc-enc0/device
lrwxrwxrwx 1 root root 0 Jan 12 22:10 /sys/class/net/wwan1/device/cdc_enc/cdc-enc0/device -> ../../../2-2:1.3


I guess we also could put that information into the uevent or some sysfs
attribute.  At the momemt there is only one class specific attribute:

 nemi:/tmp# grep . /sys/class/cdc_enc/cdc-enc0/*
 /sys/class/cdc_enc/cdc-enc0/dev:251:0
 /sys/class/cdc_enc/cdc-enc0/protocol:QMI
 /sys/class/cdc_enc/cdc-enc0/uevent:MAJOR=251
 /sys/class/cdc_enc/cdc-enc0/uevent:MINOR=0
 /sys/class/cdc_enc/cdc-enc0/uevent:DEVNAME=cdc-enc0


That's the "protocol" attribute.  We need this as this driver in theory
could be used for *any* protocol encapsulated in CDC. 

As noted above, the driver will attempt to filter out only the packets
destined for a specific client.  That is:  You can open /dev/cdc-encX
multiple times and have each client only see the replies to its own
requests + broadcasts.

But I soon found out that this was too restrictive for protocol debug
usage, so of you open the device read-only then you will get all
packets.  Including those sent by other clients, which again includes
the internal one managing the wwanX network interface.

This makes it real simple to do command line debugging.  You can have
hexdump provide readable output, while at the same time sending QMI
commands with a simple redirect.


E.g. requesting a client id for subsystem 0x05 (messaging things):
 nemi:/tmp# perl -e 'print pack("C*", map { hex } @ARGV)' 1 f 0 0 0 0 0 1 22 0 4 0 1 1 0 5 >/dev/cdc-enc0 

and releasing it again:
 nemi:/tmp# perl -e 'print pack("C*", map { hex } @ARGV)' 1 10 0 0 0 0 0 2 23 0 5 0 1 2 0 5 1 >/dev/cdc-enc0 

results in this when viewed with hexdump (I realize that you must have
debugged nearly as much QMI as me to read this, but you may notice that
there is a phone number in there):

nemi:/lib/modules/3.1.0-1-amd64/misc# hexdump -C </dev/cdc-enc0 
00000000  01 0f 00 00 00 00 00 01  22 00 04 00 01 01 00 05  |........".......|
00000010  01 17 00 80 00 00 01 01  22 00 0c 00 02 04 00 00  |........".......|
00000020  00 00 00 01 02 00 05 01  01 1e 00 80 05 01 04 00  |................|
00000030  00 46 00 12 00 01 0f 00  31 34 35 0b 2b 34 37 39  |.F......145.+479|
00000040  30 30 30 32 31 30 30 01  10 00 00 00 00 00 02 23  |0002100........#|
00000050  00 05 00 01 02 00 05 01  01 17 00 80 00 00 01 02  |................|
00000060  23 00 0c 00 02 04 00 00  00 00 00 01 02 00 05 01  |#...............|

So why was that?  Well, I am cheating so I ran a semi-decoding QMI
client at the same time (note how that works just fine, and that this
demo was done *while the network interface was up and running*). Here
you see the first request followed by a reply as expected, but then I
immediately got an "indication" which is the QMI name for unsolicited
messages.  Don't know why, but the device obviously wanted to tell me
something related to my registration as a client. The embedded phone
number is the voice mail number of my operator, so I can guess what as
well :-)

The two last messages are the client id release request and reply.


== read 16 bytes ==

0000  01 0f 00 00 00 00 00 01  22 00 04 00 01 01 00 05

.tf = 0x01
.len = 0x000f
.ctrl = 0x00 control point
.service = 0x00
.qmicid = 0x00
.flags = 0x00 request
.tid = 0x01
.msgid = 0x0022
.len = 0x0004
[0x01] (1) 05   .


== read 24 bytes ==

0000  01 17 00 80 00 00 01 01  22 00 0c 00 02 04 00 00
0010  00 00 00 01 02 00 05 01

.tf = 0x01
.len = 0x0017
.ctrl = 0x80 service
.service = 0x00
.qmicid = 0x00
.flags = 0x01 response
.tid = 0x01
.msgid = 0x0022
.len = 0x000c
[0x02] (4) SUCCESS (0x0000) QMI_ERR_NONE
[0x01] (2) 05 01        ..


== read 31 bytes ==

0000  01 1e 00 80 05 01 04 00  00 46 00 12 00 01 0f 00
0010  31 34 35 0b 2b 34 37 39  30 30 30 32 31 30 30

.tf = 0x01
.len = 0x001e
.ctrl = 0x80 service
.service = 0x05
.qmicid = 0x01
.flags = 0x04 indication
.tid = 0x0000
.msgid = 0x0046
.len = 0x0012
[0x01] (15) 31 34 35 0b 2b 34 37 39 30 30 30 32 31 30 30        145.+4790002100


== read 17 bytes ==

0000  01 10 00 00 00 00 00 02  23 00 05 00 01 02 00 05
0010  01

.tf = 0x01
.len = 0x0010
.ctrl = 0x00 control point
.service = 0x00
.qmicid = 0x00
.flags = 0x00 request
.tid = 0x02
.msgid = 0x0023
.len = 0x0005
[0x01] (2) 05 01        ..


== read 24 bytes ==

0000  01 17 00 80 00 00 01 02  23 00 0c 00 02 04 00 00
0010  00 00 00 01 02 00 05 01

.tf = 0x01
.len = 0x0017
.ctrl = 0x80 service
.service = 0x00
.qmicid = 0x00
.flags = 0x01 response
.tid = 0x02
.msgid = 0x0023
.len = 0x000c
[0x02] (4) SUCCESS (0x0000) QMI_ERR_NONE
[0x01] (2) 05 01        ..


> PS: Has somebody with this hardware access to an IPv6-enabled network? Does 
> dhcp6 or routeradvertisements work in cdc/ndis-mode?

Sorry, I don't know and don't yet have access to any IPv6 mobile
network.  

But there was recently published an informational RFC about IPv6 in GSM
(aka whatever) networks, which I found to be an excellent intro to all
this APN/GGSN/xxx circus: http://www.rfc-editor.org/rfc/rfc6459.txt


Bjørn

diff --git a/drivers/net/usb/Kconfig b/drivers/net/usb/Kconfig
index 84d4608..7c7ae46 100644
--- a/drivers/net/usb/Kconfig
+++ b/drivers/net/usb/Kconfig
@@ -458,4 +458,18 @@ config USB_VL600
 
 	  http://ubuntuforums.org/showpost.php?p=10589647&postcount=17
 
+config USB_NET_QMI_WWAN
+	tristate "USB-to-WWAN driver for QMI based 3G and LTE modems"
+	depends on USB_NET_CDCETHER
+	select USB_NET_CDC_ENC
+	help
+	  Support WWAN LTE/3G devices based on chipsets from Qualcomm,
+	  using the Qualcomm MSM Interface (QMI) protocol to
+	  configure the device.
+
+	  Note that it is still necessary to configure some aspects of
+	  the device using AT commands on a tty.  Select the option
+	  driver to get this support.
+
+
 endmenu
diff --git a/drivers/net/usb/Makefile b/drivers/net/usb/Makefile
index c203fa2..644d512 100644
--- a/drivers/net/usb/Makefile
+++ b/drivers/net/usb/Makefile
@@ -29,4 +29,7 @@ obj-$(CONFIG_USB_SIERRA_NET)	+= sierra_net.o
 obj-$(CONFIG_USB_NET_CX82310_ETH)	+= cx82310_eth.o
 obj-$(CONFIG_USB_NET_CDC_NCM)	+= cdc_ncm.o
 obj-$(CONFIG_USB_VL600)		+= lg-vl600.o
+obj-$(CONFIG_USB_NET_CDC_ENC)	+= cdc_enc.o
+obj-$(CONFIG_USB_NET_QMI_WWAN)	+= qmi_wwan.o
+qmi_wwan-objs := qmi_wwan_core.o qmi_proto.o
 
diff --git a/drivers/net/usb/cdc_enc.c b/drivers/net/usb/cdc_enc.c
new file mode 100644
index 0000000..100913a
--- /dev/null
+++ b/drivers/net/usb/cdc_enc.c
@@ -0,0 +1,486 @@
+/*
+ * Copyright (c) 2012  Bjørn Mork <bjorn@xxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ */
+
+#include <linux/module.h>
+#include <linux/cdev.h>
+#include <linux/slab.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/completion.h>
+#include <linux/usb.h>
+#include <linux/usb/cdc.h>
+#include "cdc_enc.h"
+
+/* copy received data to the client local buffer */
+static void cdc_enc_copy_to_client(struct cdc_enc_client *client, void *data, size_t len)
+{
+	/* silently ignore empty messages */
+	if (len == 0)
+		return;
+
+	pr_debug("%s() client=%p, len=%zu, flags=%lx, err_coll=%ld, err_oflow=%ld\n", __func__, 
+		client, len, client->flags, client->err_coll, client->err_oflow);
+
+	/* someone is using the buffer... count error and ignore data */
+	if (test_and_set_bit(CDC_ENC_CLIENT_BUSY, &client->flags)) {
+		client->err_coll++;
+		return;
+	}
+
+	/* overwriting unread data? count error and ignore data */
+	if (test_and_set_bit(CDC_ENC_CLIENT_RX, &client->flags)) {
+		client->err_oflow++;
+	} else {
+		memcpy(client->buf, data, len);
+		client->len = len;
+	}
+
+	clear_bit(CDC_ENC_CLIENT_BUSY, &client->flags);
+
+	/* signal new data available to any waiting reader */
+	complete(&client->ready);
+}
+
+/* URB callback, copying common recv buffer to the relevant client(s) */
+static void cdc_enc_read_callback(struct urb *urb)
+{
+	pr_debug("%s() urb->status=%d, urb->actual_length=%d \n", __func__, urb->status, urb->actual_length);
+
+	/* silently ignoring any errors */
+	if (urb->status == 0 && urb->actual_length > 0) {
+		struct cdc_enc_state *cdc_enc = (void *)urb->context;
+		struct cdc_enc_client *tmp, *client = NULL;
+
+		if (cdc_enc->proto && cdc_enc->proto->demux_lookup)
+			client = cdc_enc->proto->demux_lookup(cdc_enc, urb->transfer_buffer, urb->actual_length);
+
+		if (client)
+			cdc_enc_copy_to_client(client, urb->transfer_buffer, urb->actual_length);
+
+		/* copy to all promisc clients, or every client if no match - 
+		 *  e.g. for unsolicited indications 
+		 */
+		list_for_each_entry(tmp, &cdc_enc->clients, list) {
+			if (!client || test_bit(CDC_ENC_CLIENT_PROMISC, &tmp->flags))
+				cdc_enc_copy_to_client(tmp, urb->transfer_buffer, urb->actual_length);
+		}
+	}
+		
+}
+
+/* submit a command */
+int cdc_enc_send_sync(struct cdc_enc_client *client, unsigned char *msg, size_t len)
+{
+	int status = 0;
+	struct cdc_enc_state *cdc_enc = client->cdc_enc;
+	struct cdc_enc_client *tmp;
+
+	/* copy to promiscuous clients */
+	list_for_each_entry(tmp, &cdc_enc->clients, list) {
+		if (test_bit(CDC_ENC_CLIENT_PROMISC, &tmp->flags))
+			cdc_enc_copy_to_client(tmp, msg, len);
+	}
+
+	/* register our callback for this transaction */
+	if (cdc_enc->proto && cdc_enc->proto->demux_register)
+		status = cdc_enc->proto->demux_register(client, msg, len);
+
+	if (status == 0)
+		status = usb_control_msg(cdc_enc->dev,
+				usb_sndctrlpipe(cdc_enc->dev, 0),
+				USB_CDC_SEND_ENCAPSULATED_COMMAND, USB_DIR_OUT|USB_TYPE_CLASS|USB_RECIP_INTERFACE,
+				0, cdc_enc->setup.wIndex,
+				msg, len, 1000);
+
+	pr_debug("%s() status=%d, len=%zu \n", __func__, status, len);
+
+	return status;
+}
+EXPORT_SYMBOL_GPL(cdc_enc_send_sync);
+
+struct cdc_enc_client *cdc_enc_add_client(struct cdc_enc_state *cdc_enc)
+{
+	struct cdc_enc_client *client;
+
+	pr_debug("%s()\n", __func__);
+	
+	client = kzalloc(sizeof(struct cdc_enc_client), GFP_KERNEL);
+	if (!client)
+		goto done;
+
+	client->cdc_enc = cdc_enc;
+	init_completion(&client->ready);
+
+	mutex_lock(&cdc_enc->clients_lock);
+	list_add(&client->list, &cdc_enc->clients);
+	mutex_unlock(&cdc_enc->clients_lock);
+
+done:
+	pr_debug("%s(), added new client=%p\n", __func__, client);
+	return client;
+}
+EXPORT_SYMBOL_GPL(cdc_enc_add_client);
+
+void cdc_enc_destroy_client(struct cdc_enc_client *client)
+{
+	struct cdc_enc_transaction *tmp, *q;
+	struct list_head *head;
+	struct cdc_enc_state *cdc_enc = client->cdc_enc;
+
+	pr_debug("%s()\n", __func__);
+
+	/* delete any pending transactions */
+	mutex_lock(&cdc_enc->tmap_lock);
+	head = &cdc_enc->tmap;
+
+	list_for_each_entry_safe(tmp, q,  head, list)
+		if (tmp->client == client) {
+			list_del(&tmp->list);
+			kfree(tmp);
+		}
+	mutex_unlock(&cdc_enc->tmap_lock);
+
+	/* delete client */
+	mutex_lock(&cdc_enc->clients_lock);
+	list_del(&client->list);
+	pr_debug("%s() removed client=%p\n", __func__, client);
+	kfree(client);
+	mutex_unlock(&cdc_enc->clients_lock);
+	wake_up(&cdc_enc->waitq);
+}
+EXPORT_SYMBOL_GPL(cdc_enc_destroy_client);
+
+/* ----- file operations ----- */
+
+static ssize_t cdc_enc_fops_read(struct file *file, char __user *buf, size_t size, loff_t *pos)
+{
+	struct cdc_enc_client *client = file->private_data;
+	int ret = -EFAULT;
+
+	pr_debug("%s()\n", __func__);
+
+	if (!client || !client->cdc_enc)
+		return -ENODEV;
+
+	dev_dbg(&client->cdc_enc->dev->dev,"%s(), size=%zu, pos=%p, len=%zu\n", __func__, size, pos, client->len);
+
+	while (!test_bit(CDC_ENC_CLIENT_RX, &client->flags) && !test_bit(CDC_ENC_CLIENT_BUSY, &client->flags)) {/* no data */
+		/* submit read URB to force update */
+		cdc_enc_recv(client->cdc_enc);
+
+		ret = wait_for_completion_interruptible_timeout(&client->ready, 100);
+		if (ret < 0)
+			return ret;
+
+	}
+
+	/* someone is using the buffer... return */
+	if (test_and_set_bit(CDC_ENC_CLIENT_BUSY, &client->flags))
+		return 0;
+
+	if (client->len > size) {
+		ret = -EFAULT;
+		goto err;
+	}
+
+	ret = client->len - copy_to_user(buf, client->buf, client->len);
+
+err:
+	/* order is important! */
+	clear_bit(CDC_ENC_CLIENT_RX, &client->flags);
+	clear_bit(CDC_ENC_CLIENT_BUSY, &client->flags);
+
+	return ret;
+}
+
+static ssize_t cdc_enc_fops_write(struct file *file, const char __user * buf, size_t size, loff_t *pos)
+{
+	ssize_t len;
+	struct cdc_enc_client *client = file->private_data;
+
+	pr_debug("%s()\n", __func__);
+
+	if (!client || !client->cdc_enc)
+		return -ENODEV;
+
+	dev_dbg(&client->cdc_enc->dev->dev,"%s(), size=%zu, pos=%p\n", __func__, size, pos);
+
+	if (size > CDC_ENC_BUFLEN)
+		return -EFAULT;
+
+	/* are someone using our buffer? */
+	if (test_and_set_bit(CDC_ENC_CLIENT_BUSY, &client->flags))
+		return -ERESTARTSYS;
+	
+	if (copy_from_user(client->buf, buf, size))
+		return -EFAULT;
+
+	/* send to the device */
+	len = cdc_enc_send_sync(client, client->buf, size);
+
+	clear_bit(CDC_ENC_CLIENT_BUSY, &client->flags);
+	return len;
+}
+
+static int cdc_enc_fops_open(struct inode *inode, struct file *file)
+{
+	struct cdc_enc_state *cdc_enc;
+	struct cdc_enc_client *client;
+
+	pr_debug("%s()\n", __func__);
+
+	/* associate the file with our backing CDC_ENC device */
+	cdc_enc = container_of(inode->i_cdev, struct cdc_enc_state, cdev);
+	if (!cdc_enc)
+		return -ENODEV;
+
+	dev_dbg(&cdc_enc->dev->dev,"%s() cdc_enc=%p\n", __func__, cdc_enc);
+
+	/* set up a ring buffer to receive our readable data? */
+	client = cdc_enc_add_client(cdc_enc);
+
+	if (!client)
+		return -ENOMEM;
+
+	/* opening read-only sets promiscuous mode */
+	if ((file->f_flags & O_ACCMODE) == O_RDONLY)
+		set_bit(CDC_ENC_CLIENT_PROMISC, &client->flags);
+
+	file->private_data = client;
+	return 0;
+}
+
+static int cdc_enc_fops_release(struct inode *inode, struct file *file)
+{
+	struct cdc_enc_client *client = file->private_data;
+	struct cdc_enc_state *cdc_enc = client->cdc_enc;
+
+	pr_debug("%s()\n", __func__);
+
+	if (!client || !cdc_enc)
+		return -ENODEV;
+
+	set_bit(CDC_ENC_CLIENT_BUSY, &client->flags);
+	complete_all(&client->ready);
+
+	cdc_enc_destroy_client(client);
+	file->private_data = NULL;
+	return 0;
+}
+
+static struct file_operations cdc_enc_fops = {
+	.owner   = THIS_MODULE,
+	.read    = cdc_enc_fops_read,
+	.write   = cdc_enc_fops_write,
+	.open    = cdc_enc_fops_open,
+	.release = cdc_enc_fops_release,
+	.llseek  = noop_llseek,
+};
+
+/* receive a QMUX can be called in interrupt context! */
+int cdc_enc_recv(struct cdc_enc_state *cdc_enc)
+{
+	int ret = usb_submit_urb(cdc_enc->urb, GFP_ATOMIC);
+
+	pr_debug("%s() returns %d\n", __func__, ret);
+	return ret;
+
+}
+EXPORT_SYMBOL_GPL(cdc_enc_recv);
+
+int cdc_enc_killurb(struct cdc_enc_state *cdc_enc)
+{
+	pr_debug("%s() going to call usb_kill_urb() now \n", __func__);
+	usb_kill_urb(cdc_enc->urb);
+	pr_debug("%s() usb_kill_urb() has returned\n", __func__);
+	return 0;
+}
+
+EXPORT_SYMBOL_GPL(cdc_enc_killurb);
+
+/* global state relating to the character device */
+static struct class *cdc_enc_class;	/* registered device class */
+static dev_t cdc_enc_dev0;		/* our allocated major/minor range */
+static unsigned long cdc_enc_minor;	/* bitmap of minors in use */
+
+static ssize_t 
+protocol_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+        struct cdc_enc_state *cdc_enc = dev_get_drvdata(dev);
+
+        return sprintf(buf, "%s\n", cdc_enc->proto ? cdc_enc->proto->name : "none" );
+}
+static struct device_attribute cdc_enc_dev_attrs[] = {
+	__ATTR_RO(protocol),
+        __ATTR_NULL
+};
+
+/* allocate and initiate a CDC_ENC state device */
+struct cdc_enc_state *cdc_enc_init_one(struct usb_interface *intf, const struct cdc_enc_protocol *proto)
+{
+	struct cdc_enc_state *cdc_enc;
+	dev_t devno;
+	int i, ret;
+
+	pr_debug("%s()\n", __func__);
+
+	/* find an unused minor */
+	for (i = 0; i < CDC_ENC_MAX_MINOR; i++)
+		if (!test_and_set_bit(i, &cdc_enc_minor))
+			break;
+
+	/* no free devices */
+	if (i == CDC_ENC_MAX_MINOR)
+		goto err_nodev;
+
+	cdc_enc = kzalloc(sizeof(struct cdc_enc_state), GFP_KERNEL);
+	if (!cdc_enc)
+		goto err_nodev;
+
+	/* make device number */
+	devno = cdc_enc_dev0 + i;
+
+	/*
+	 * keep track of the device receiving the control messages and the
+	 * number of the CDC (like) control interface which is our target.
+	 * Note that the interface might be disguised as vendor specific,
+	 * and be a combined CDC control/data interface
+	 */
+	cdc_enc->dev = interface_to_usbdev(intf);
+	/* FIXME: it would be useful to verify that this interface actually talks CDC */
+
+	/* store handlers */
+	cdc_enc->proto = proto;
+
+	/* create async receive URB */
+	cdc_enc->urb = usb_alloc_urb(0, GFP_KERNEL);
+	if (!cdc_enc->urb)
+		goto err_urb;
+
+	/* usb control setup */
+	cdc_enc->setup.bRequestType = USB_DIR_IN|USB_TYPE_CLASS|USB_RECIP_INTERFACE;
+	cdc_enc->setup.bRequest = USB_CDC_GET_ENCAPSULATED_RESPONSE;
+	cdc_enc->setup.wValue = 0; /* zero */
+	cdc_enc->setup.wIndex = intf->cur_altsetting->desc.bInterfaceNumber;
+	cdc_enc->setup.wLength = CDC_ENC_BUFLEN;
+
+	/* prepare the async receive URB */
+	usb_fill_control_urb(cdc_enc->urb, cdc_enc->dev,
+			usb_rcvctrlpipe(cdc_enc->dev, 0),
+			(char *)&cdc_enc->setup,
+			cdc_enc->rcvbuf,
+			CDC_ENC_BUFLEN,
+			cdc_enc_read_callback, cdc_enc);
+
+
+        init_waitqueue_head(&cdc_enc->waitq);
+
+	/* initialize client list */
+	INIT_LIST_HEAD(&cdc_enc->clients);
+	mutex_init(&cdc_enc->clients_lock);
+
+	/* initialize transaction map */
+	INIT_LIST_HEAD(&cdc_enc->tmap);
+	mutex_init(&cdc_enc->tmap_lock);
+
+	/* finally, create the character device, using the interface as a parent dev */
+        cdev_init(&cdc_enc->cdev, &cdc_enc_fops);
+	ret = cdev_add(&cdc_enc->cdev, devno, 1);
+	if (ret < 0)
+		goto err_cdev;
+
+	device_create(cdc_enc_class, &intf->dev, devno, cdc_enc, "cdc-enc%d", i);
+
+	return cdc_enc;
+
+err_cdev:
+	usb_free_urb(cdc_enc->urb);
+err_urb:
+	kfree(cdc_enc);
+err_nodev:
+	return NULL;
+}
+EXPORT_SYMBOL_GPL(cdc_enc_init_one);
+
+/* disable and free a CDC_ENC state device */
+int cdc_enc_free_one(struct cdc_enc_state *cdc_enc)
+{
+	struct cdc_enc_client *tmp;
+
+	pr_debug("%s()\n", __func__);
+
+
+	/* wait for all clients to exit first ... */
+	list_for_each_entry(tmp, &cdc_enc->clients, list)
+		pr_debug("%s() waiting for client %p with flags=0x%08lx\n", __func__, tmp, tmp->flags);
+
+	if (wait_event_interruptible(cdc_enc->waitq, list_empty(&cdc_enc->clients)) < 0)
+		pr_debug("%s() interrupted...\n", __func__);
+	
+	/* delete character device */
+	device_destroy(cdc_enc_class, cdc_enc->cdev.dev);
+	cdev_del(&cdc_enc->cdev);
+
+	/* kill any pending recv urb */
+	pr_debug("%s() going to call usb_kill_urb() now \n", __func__);
+
+	usb_kill_urb(cdc_enc->urb);
+	usb_free_urb(cdc_enc->urb);
+	pr_debug("%s() usb_kill_urb() has returned\n", __func__);
+
+	/* mark minor available again */
+	clear_bit(MINOR(cdc_enc->cdev.dev) - MINOR(cdc_enc_dev0), &cdc_enc_minor);
+
+	/* release this slot */
+	kfree(cdc_enc);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(cdc_enc_free_one);
+
+
+/* XXX: ref /usr/local/src/git/linux/drivers/char/bsr.c */
+ 
+static int __init cdc_enc_init(void)
+{
+        int ret;
+
+	pr_debug("%s()\n", __func__);
+
+        ret = alloc_chrdev_region(&cdc_enc_dev0, 0, CDC_ENC_MAX_MINOR, "cdc_enc");
+        if (ret < 0)
+                goto err_region;
+
+	/* create a chardev class */
+	cdc_enc_class = class_create(THIS_MODULE, "cdc_enc");
+        if (IS_ERR(cdc_enc_class)) {
+                ret = PTR_ERR(cdc_enc_class);
+                goto err_class;
+        }
+	cdc_enc_class->dev_attrs = cdc_enc_dev_attrs;
+	return 0;
+
+err_class:
+        unregister_chrdev_region(cdc_enc_dev0, CDC_ENC_MAX_MINOR);
+err_region:
+	return ret;
+}
+module_init(cdc_enc_init);
+
+static void __exit cdc_enc_exit(void)
+{
+	pr_debug("%s()\n", __func__);
+
+	class_destroy(cdc_enc_class);
+	unregister_chrdev_region(cdc_enc_dev0, CDC_ENC_MAX_MINOR);
+}
+module_exit(cdc_enc_exit);
+
+MODULE_AUTHOR("Bjørn Mork <bjorn@xxxxxxx>");
+MODULE_DESCRIPTION("CDC Encapsulated Command Driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/usb/cdc_enc.h b/drivers/net/usb/cdc_enc.h
new file mode 100644
index 0000000..3c84002
--- /dev/null
+++ b/drivers/net/usb/cdc_enc.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2012  Bjørn Mork <bjorn@xxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ */
+
+#define CDC_ENC_BUFLEN 512
+#define CDC_ENC_MAX_MINOR 16
+
+enum cdc_enc_client_flag {
+	CDC_ENC_CLIENT_BUSY = 0,	/* buffer is in use for read or write */
+	CDC_ENC_CLIENT_RX,		/* new data available */
+	CDC_ENC_CLIENT_PROMISC,		/* this client wants all messages */
+	CDC_ENC_CLIENT_PRIVATE,		/* the rest is available for private use */
+};
+
+/* per client data */
+struct cdc_enc_client {
+	unsigned char buf[CDC_ENC_BUFLEN];	/* combined rx/tx buffer */
+	size_t len;			/* length of data in buffer */
+	unsigned long flags;
+	unsigned long err_oflow;	/* new data before the previous was read */
+	unsigned long err_coll;		/* new data dropped because buffer was busy */
+	struct cdc_enc_state *cdc_enc;		/* CDC_ENC instance owning the client */
+	struct completion ready;
+	struct list_head list;		/* linux/list.h pointers */
+	u8 priv[8];			/* client specific data */
+};
+
+/* list of pending transactions */
+struct cdc_enc_transaction {
+	__le16 key;			/* transaction id */
+	struct cdc_enc_client *client;	/* client owning this transaction */
+	struct list_head list;		/* linux/list.h pointers */
+};
+
+struct cdc_enc_protocol {
+	const char *name;
+	int (*demux_register)(struct cdc_enc_client *client, void *cmd, size_t len);
+	struct cdc_enc_client *(*demux_lookup)(struct cdc_enc_state *cdc_enc, void *data, size_t len);
+};
+
+/* per CDC_ENC interface state */
+struct cdc_enc_state {
+	struct usb_device *dev;
+	struct urb *urb;		/* receive urb */
+	unsigned char rcvbuf[CDC_ENC_BUFLEN];     /* receive buffer */
+	struct usb_ctrlrequest setup;	/* the receive setup - 8 bytes */
+	struct list_head tmap;		/* transaction => callback map */
+	struct mutex tmap_lock;
+	struct list_head clients;	/* list of clients - first entry is wwan client */
+	struct mutex clients_lock;
+	struct cdev cdev;		/* registered character device */
+	const struct cdc_enc_protocol *proto;
+        wait_queue_head_t       waitq;
+};
+
+/* must be called whenever USB_CDC_NOTIFY_RESPONSE_AVAILABLE is received */
+extern int cdc_enc_recv(struct cdc_enc_state *cdc_enc);
+extern int cdc_enc_killurb(struct cdc_enc_state *cdc_enc);
+
+/* initialize */
+extern struct cdc_enc_state *cdc_enc_init_one(struct usb_interface *intf, const struct cdc_enc_protocol *proto);
+
+/* clean up */
+extern int cdc_enc_free_one(struct cdc_enc_state *cdc_enc);
+
+/* send a synchronous message from client */
+extern int cdc_enc_send_sync(struct cdc_enc_client *client, unsigned char *msg, size_t len);
+
+/* get a new client */
+extern struct cdc_enc_client *cdc_enc_add_client(struct cdc_enc_state *cdc_enc);
+
+extern void cdc_enc_destroy_client(struct cdc_enc_client *client);
diff --git a/drivers/net/usb/cdc_ether.c b/drivers/net/usb/cdc_ether.c
index c924ea2..9b12465 100644
--- a/drivers/net/usb/cdc_ether.c
+++ b/drivers/net/usb/cdc_ether.c
@@ -211,7 +211,11 @@ int usbnet_generic_cdc_bind(struct usbnet *dev, struct usb_interface *intf)
 
 			/* a data interface altsetting does the real i/o */
 			d = &info->data->cur_altsetting->desc;
-			if (d->bInterfaceClass != USB_CLASS_CDC_DATA) {
+			if ((d->bInterfaceClass != USB_CLASS_CDC_DATA) &&
+				/* Allow vendor specific data interface iff
+				   control interface is vendor specific */
+				!(info->control->cur_altsetting->desc.bInterfaceClass == USB_CLASS_VENDOR_SPEC &&
+					d->bInterfaceClass == USB_CLASS_VENDOR_SPEC)) {
 				dev_dbg(&intf->dev, "slave class %u\n",
 					d->bInterfaceClass);
 				goto bad_desc;
diff --git a/drivers/net/usb/qmi_proto.c b/drivers/net/usb/qmi_proto.c
new file mode 100644
index 0000000..9874660
--- /dev/null
+++ b/drivers/net/usb/qmi_proto.c
@@ -0,0 +1,586 @@
+/*
+ * Copyright (c) 2012  Bjørn Mork <bjorn@xxxxxxx>
+ * Copyright (c) 2010, Code Aurora Forum. All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ */
+
+/*
+ * Dealing with devices using Qualcomm MSM Interface (QMI) for
+ * configuration.  Full documentation of the protocol can be obtained
+ * from http://developer.qualcomm.com/
+ *
+ * Some of this code may be inspired by code from the Qualcomm Gobi
+ * 2000 and Gobi 3000 drivers, available at http://www.codeaurora.org/
+ */
+
+#include <linux/module.h>
+#include <linux/cdev.h>
+#include <linux/slab.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/completion.h>
+#include <linux/usb.h>
+#include <linux/usb/cdc.h>
+#include "cdc_enc.h"
+#include "qmi_proto.h"
+
+/* find and return a pointer to the requested tlv */
+static struct qmi_tlv *qmi_get_tlv(u8 type, u8 *buf, size_t len)
+{
+	u8 *p;
+	struct qmi_tlv *t;
+
+	for (p = buf; p < buf + len; p += t->len + sizeof(struct qmi_tlv)) {
+		t = (struct qmi_tlv *)p;
+		if (t->type == type)
+			return (p + t->len <= buf + len) ? t : NULL;
+	}
+	return NULL;
+}
+
+/* TLV 0x02 is status: return a negative QMI error code or 0 if OK */
+static int qmi_verify_status_tlv(u8 *buf, size_t len)
+{
+	struct qmi_tlv *tlv = qmi_get_tlv(0x02, buf, len);
+	struct qmi_tlv_response_data *r = (void *)tlv->bytes;
+
+	/* OK: indications and requests do not have any status TLV */
+	if (!tlv)
+		return 0;
+
+	if (tlv->len != sizeof(struct qmi_tlv_response_data))
+		return -1; /* QMI_ERR_MALFORMED_MSG */
+
+	return r->error ? -r->code : 0;
+}
+
+/* verify QMUX message header and return pointer to the (first) message if OK */
+static struct qmi_msg *qmi_qmux_verify(u8 *data, size_t len)
+{
+	struct qmux_header *h =  (void *)data;
+	struct qmi_msg *m = NULL;
+
+	if (len < sizeof(struct qmux_header) || /* short packet */
+		h->tf != 0x01 || h->len != (len - 1)) /* invalid QMUX packet! */
+		goto err;
+
+	/* tid has a different size for QMI_CTL for some fucking stupid reason */
+	if (h->service == QMI_CTL) {
+		struct qmi_ctl *ctl = (void *)data;
+		if (len >= sizeof(struct qmi_ctl) +  ctl->m.len)
+			m = &ctl->m;
+	} else {
+		struct qmi_wds *wds = (void *)data;
+		if (len >= sizeof(struct qmi_wds) +  wds->m.len)
+			m = &wds->m;
+	}
+
+err:
+	return m;
+}
+
+/* parse a QMI_WDS indications looking connection status updates */
+static int qmi_wds_parse(struct cdc_enc_client *wwan, struct qmi_msg *m)
+{
+	int status;
+	struct qmi_tlv *tlv;
+
+	if (!m)
+		return -1;
+
+	/* check and save status TLV */
+	status = qmi_verify_status_tlv(m->tlv, m->len);
+
+	if (status < 0)
+		return status;
+
+	switch (m->msgid) {
+	case 0x0022: /* QMI_WDS_GET_PKT_SRVC_STATUS */
+		/*
+		 * TLV 0x01 is a 2 byte connection status.
+		 * Indications use the second byte for a "reconfiguration" flag
+		 */
+		tlv = qmi_get_tlv(0x01, m->tlv, m->len);
+
+		/* connection dropped? */
+		if (tlv && tlv->len == 2 && tlv->bytes[0] != 2)
+			clear_bit(QMI_WWAN_UP, &wwan->flags);
+	}
+
+	return status;
+}
+
+/* wwan client indication processing */
+static int qmi_wwan_rx(struct cdc_enc_client *client)
+{
+	struct qmux_header *h;
+	struct qmi_msg *m;
+	int ret = -1;
+
+	/* any unread data available? */
+	if (!test_bit(CDC_ENC_CLIENT_RX, &client->flags))
+		return 0;
+
+	/* someone is using the buffer... count error and return */
+	if (test_and_set_bit(CDC_ENC_CLIENT_BUSY, &client->flags)) {
+		client->err_coll++;
+		goto busy;
+	}
+
+	h = (struct qmux_header *)client->buf;
+	m = qmi_qmux_verify(client->buf, client->len);
+	if (!m)
+		goto done;
+
+	switch (h->service) {
+	case QMI_WDS:
+		ret = qmi_wds_parse(client, m);
+		break;
+	}
+
+done:
+	/* order is important! */
+	clear_bit(CDC_ENC_CLIENT_RX, &client->flags);
+	clear_bit(CDC_ENC_CLIENT_BUSY, &client->flags);
+busy:
+	return ret;
+}
+
+
+/* --- helpers for multiplexing multiple simultaneous clients --- */
+
+/* register a client for a CDC encapsulated command */
+static int qmi_demux_register(struct cdc_enc_client *client, void *cmd, size_t len)
+{
+	struct qmi_ctl *ctl = cmd;
+	struct cdc_enc_transaction *tmp;
+	struct list_head *head = &client->cdc_enc->tmap;
+	int ret = -EINVAL;
+	__le16 key;
+
+	/* verify that cmd is a valid QMUX containing a single message */
+	if (!qmi_qmux_verify(cmd, len))
+		goto err;
+
+	/* use transaction id for CTL, otherwise use system + cid */
+	if (ctl->h.service == QMI_CTL)
+		key = ctl->tid;
+	else 
+		key = (ctl->h.service << 8 | ctl->h.qmicid);
+
+	/* don't register duplicates */
+	list_for_each_entry(tmp, head, list) {
+		if (tmp->key == key)
+			goto ret;
+	}
+
+	/* register client as the owner of this key */
+	tmp = kmalloc(sizeof(struct cdc_enc_transaction), GFP_KERNEL);
+	if (!tmp) {
+		ret = -ENOMEM;
+		goto err;
+	}
+
+	tmp->key = key;
+	tmp->client = client;
+
+	mutex_lock(&client->cdc_enc->tmap_lock);
+	list_add_tail(&tmp->list, head);
+	mutex_unlock(&client->cdc_enc->tmap_lock);
+	pr_debug("%s() adding client=%p for key=0x%04x\n", __func__, client, tmp->key);
+
+ret:
+	ret = 0;
+err:
+	return ret;
+}
+
+/* lookup a transaction id and return the client owning it or NULL */
+static struct cdc_enc_client *qmi_demux_lookup(struct cdc_enc_state *cdc_enc, void *data, size_t len)
+{
+	struct qmi_ctl *ctl = data;
+	struct cdc_enc_transaction *tmp;
+	struct cdc_enc_client *ret = NULL;
+	__le16 key;
+
+	if (len < sizeof(struct qmi_ctl))
+		goto ret;
+
+	/* copy broadcast cid to all clients */
+	if (ctl->h.qmicid == 0xff)
+		goto ret;
+
+	/* use transaction id for CTL, otherwise use system + cid */
+	if (ctl->h.service == QMI_CTL)
+		key = ctl->tid;
+	else 
+		key = ctl->h.service << 8 | ctl->h.qmicid;
+	
+	/* FIXME: lock list against changes */
+	list_for_each_entry(tmp, &cdc_enc->tmap, list) {
+		if (tmp->key == key) {
+			ret = tmp->client;
+			break;
+		}
+	}
+
+	/* transaction based keys are one-time only */
+	if (ret && ctl->h.service == QMI_CTL) {
+		list_del(&tmp->list);
+		kfree(tmp);
+	}
+
+	pr_debug("%s() found client=%p for key=0x%04x\n", __func__, ret, key);
+ret:
+	return ret;
+}
+
+/* --- creating QMI messages --- */
+
+/* get a new transaction id */
+static __le16 new_tid(void)
+{
+	static __le16 tid;
+	return ++tid;
+}
+
+/* assemble a QMI_WDS packet */
+static size_t qmi_create_wds_msg(struct cdc_enc_client *client, __le16 msgid, struct qmi_tlv *tlv)
+{
+	struct qmi_wds *wds =  (void *)client->buf;
+
+	if (!client->priv[5])
+		return 0;
+
+	memset(wds, 0, sizeof(*wds));
+	wds->h.tf = 1;     /* always 1 */
+	wds->h.service = QMI_WDS;
+        wds->h.qmicid = client->priv[5];
+	wds->h.len = sizeof(*wds) - 1;
+	wds->tid = new_tid();
+	wds->m.msgid = msgid;
+	if (tlv) {
+		ssize_t tlvsize = tlv->len + sizeof(struct qmi_tlv);
+		memcpy(wds->m.tlv, tlv, tlvsize);
+		wds->m.len = tlvsize;
+		wds->h.len += tlvsize;
+	}
+	return wds->h.len + 1;
+}
+
+/* assemble a QMI_CTL packet */
+static size_t qmi_create_ctl_msg(struct cdc_enc_client *client, __le16 msgid, struct qmi_tlv *tlv)
+{
+	struct qmi_ctl *ctl =  (void *)client->buf;
+
+	memset(ctl, 0, sizeof(*ctl));
+	ctl->h.tf = 1;     /* always 1 */
+	ctl->h.len = sizeof(*ctl) - 1;
+	while (ctl->tid == 0 || ctl->tid == 0xff) /* illegal values */
+		ctl->tid = new_tid() & 0xff;
+	ctl->m.msgid = msgid;
+	if (tlv) {
+		ssize_t tlvsize = tlv->len + sizeof(struct qmi_tlv);
+		memcpy(ctl->m.tlv, tlv, tlvsize);
+		ctl->m.len = tlvsize;
+		ctl->h.len += tlvsize;
+	}
+	return ctl->h.len + 1;
+
+}
+
+/* send the specified message and wait for reply unless timeout is 0
+ * returns a pointer to TLV 0x01 with CDC_ENC_CLIENT_BUSY flag set
+ */
+static struct qmi_tlv *qmi_send_and_wait_for_reply(struct cdc_enc_client *client, u8 system, __le16 msgid, struct qmi_tlv *tlv, int timeout)
+{
+	struct qmi_wds *wds;
+	struct qmi_msg *msg;
+	size_t len = 0;
+	__le16 tid;
+
+	/* lock client buffer */
+	if (test_and_set_bit(CDC_ENC_CLIENT_BUSY, &client->flags))
+		return NULL;
+
+	/* create message */
+	switch (system) {
+	case QMI_CTL:
+		len = qmi_create_ctl_msg(client, msgid, tlv);
+		break;
+	case QMI_WDS:
+		len = qmi_create_wds_msg(client, msgid, tlv);
+		break;
+	}
+
+	if (!len)
+		goto err;
+
+	/* ignoring that this will match on part of the msgid as well for QMI_CTL */
+	wds = (struct qmi_wds *)client->buf;
+	tid = wds->tid;
+
+	/* send it */
+	if (cdc_enc_send_sync(client, client->buf, len) != len)
+		goto err;
+
+	/* must clear busy flag to receive anything */
+	clear_bit(CDC_ENC_CLIENT_BUSY, &client->flags);
+
+	/* do the caller want us to wait for a reply? */
+	if (!timeout)
+		goto err;
+
+	/* give the device some slack before requesting a reply */
+	msleep(50);
+
+	/* wait a while - the device might already be processing other requests */
+	do {
+		/* request reply */
+//		if (cdc_enc_recv(client->cdc_enc) < 0)
+//			goto err;
+		cdc_enc_recv(client->cdc_enc);
+
+		/* wait for new data */
+		wait_for_completion_interruptible_timeout(&client->ready, 100);
+
+		/* verify that we got what we were waiting for */
+		if (test_bit(CDC_ENC_CLIENT_RX, &client->flags)) {
+			msg = qmi_qmux_verify(client->buf, client->len);
+			if (msg) {
+				wds = (struct qmi_wds *)client->buf;
+
+				/* return on exact match */
+				if (wds->tid == tid) {
+					pr_debug("%s() got match for 0x%02x msgid 0x%04x tid 0x%04x\n", __func__, system, msgid, tid);
+					return qmi_get_tlv(0x01, msg->tlv, msg->len);
+				}
+			}
+
+			/* silently ignore any other messages */
+			clear_bit(CDC_ENC_CLIENT_RX, &client->flags);
+		}
+	} while (timeout-- > 0);
+
+err:
+	/* finished using the client buffer */
+	clear_bit(CDC_ENC_CLIENT_BUSY, &client->flags);
+
+	/* no reply */
+	return NULL;
+}
+
+/* QMI_CTL msg 0x0022 is "request cid", TLV 0x01 is system */
+static int qmi_ctl_request_wds_cid(struct cdc_enc_client *client)
+{
+	struct qmi_tlv *tlv;
+	static struct qmi_tlv tlvreq = {
+		.type = 0x01,
+		.len = 1,
+		.bytes = { QMI_WDS },
+	};
+
+	/* return immediately if a CID is already allocated */
+	if (client->priv[5])
+		goto ret;
+
+	tlv = qmi_send_and_wait_for_reply(client, QMI_CTL, 0x0022, &tlvreq, 30);
+
+	/* TLV 0x01 contains system + cid */
+	if (tlv && tlv->len == 2) {
+		client->priv[5] = tlv->bytes[1];
+		clear_bit(CDC_ENC_CLIENT_RX, &client->flags);
+	}
+
+ret:
+	return client->priv[5];
+}
+
+/* QMI_WDS msg 0x0020 is "QMI_WDS_START_NETWORK_INTERFACE" */
+static int qmi_wds_start(struct cdc_enc_client *client)
+{
+	struct qmi_tlv *tlv;
+	int ret = 0;
+
+	/* NOTE: long timeout as this may require network registration++ */
+	tlv = qmi_send_and_wait_for_reply(client, QMI_WDS, 0x0020, NULL, 90);
+
+	/* TLV 0x01 is a 4 byte packet data handle we need to keep */
+	if (tlv && tlv->len == 4) {
+		/* using the first 4 private bytes for the handle */
+		memcpy(client->priv, tlv->bytes, 4);
+		set_bit(QMI_WWAN_UP, &client->flags);
+		clear_bit(CDC_ENC_CLIENT_RX, &client->flags);
+		ret = 1;
+	}
+
+	return ret;
+}
+
+/* QMI_WDS msg 0x0021 is "QMI_WDS_STOP_NETWORK_INTERFACE" */
+static int qmi_wds_stop(struct cdc_enc_client *client)
+{
+	static struct qmi_tlv tlvreq = {
+		.type = 0x01,
+		.len = 4,
+		.bytes = { 0xff, 0xff, 0xff, 0xff },
+	};
+
+	/* use specific handle if set */
+	if (client->priv[0] || client->priv[1] || client->priv[2] || client->priv[3])
+		memcpy(&tlvreq.bytes, client->priv, 4);
+
+	/* NOTE: ignoring result - can't fix failure to disconnect anyway */
+	qmi_send_and_wait_for_reply(client, QMI_WDS, 0x0021, &tlvreq, 0);
+
+	return 0;
+}
+
+/* QMI_WDS msg 0x0022 is "QMI_WDS_GET_PKT_SRVC_STATUS" */
+static int qmi_wds_status(struct cdc_enc_client *client)
+{
+	struct qmi_tlv *tlv;
+	int ret = 0;
+
+	/* NOTE: long timeout as this may require network registration++ */
+	tlv = qmi_send_and_wait_for_reply(client, QMI_WDS, 0x0022, NULL, 30);
+
+	/* TLV 0x01 is a 1 byte connection status */
+	if (tlv && tlv->len == 1) {
+		ret = tlv->bytes[0];
+		if (ret == 2)
+			set_bit(QMI_WWAN_UP, &client->flags);
+		else 
+			clear_bit(QMI_WWAN_UP, &client->flags);
+
+		clear_bit(CDC_ENC_CLIENT_RX, &client->flags);
+	}
+
+	return ret;
+}
+
+/* QMI_CTL msg 0x0023 is "release cid", TLV 0x01 is system + cid */
+static int qmi_ctl_release_wds_cid(struct cdc_enc_client *client)
+{
+	static struct qmi_tlv tlvreq = {
+		.type = 0x01,
+		.len = 2,
+		.bytes = { QMI_WDS, 0 },
+	};
+
+	if (client->priv[5]) {
+		tlvreq.bytes[1] = client->priv[5];
+		qmi_send_and_wait_for_reply(client, QMI_CTL, 0x0023, &tlvreq, 0);
+	}
+
+	/* always clear - cant reuse CID in any case */
+	client->priv[5] = 0;
+	return 0;
+}
+
+/*
+ * return current connection state with side effects: will run through
+ *  the states from INIT to CONN if possible
+ */
+int qmi_conn_state(struct cdc_enc_client *wwan)
+{
+	struct device *dev = &wwan->cdc_enc->dev->dev;
+
+	dev_dbg(dev, ".wds_cid=0x%02x, .flags=0x%08lx, .handle=0x%08x\n",
+		wwan->priv[5],
+		wwan->flags,
+		*(__le32 *)wwan->priv);
+
+	/* parse any received data */
+	qmi_wwan_rx(wwan);
+
+	if (test_bit(QMI_WWAN_DISABLE, &wwan->flags))
+		return -1;
+
+	if (test_bit(QMI_WWAN_UP, &wwan->flags))
+		return 0;
+
+	/* else attempt to connect: 1) request CID: */
+	if (qmi_ctl_request_wds_cid(wwan) < 0) {
+		/* FATAL ERROR - no way out except for unplugging the device */
+		dev_err(dev, "unable to get WDS client ID - device must be unplugged\n");
+		set_bit(QMI_WWAN_DISABLE, &wwan->flags);
+		return -1;
+	}
+
+	/* check status */
+	if (qmi_wds_status(wwan) == 1) { /* DISCONNECTED - attempt to connect */
+		if (qmi_wds_start(wwan))
+			return 0;
+
+		dev_notice(dev, "Not conneced - might need PIN code first?\n");
+	}
+
+	return 1;  /* not connected yet - */
+}
+
+int qmi_reset(struct cdc_enc_client *wwan)
+{
+	pr_debug("%s()\n", __func__);
+	/* NOTE:  We do NOT clear the allocated CID! */
+	memset(wwan->priv, 0, 5);
+	wwan->flags = 0;
+	return 1;
+}
+
+static void qmi_shutdown(struct cdc_enc_client *wwan)
+{
+	pr_debug("%s()\n", __func__);
+	qmi_ctl_release_wds_cid(wwan);
+}
+
+int qmi_suspend(struct cdc_enc_client *wwan)
+{
+	pr_debug("%s()\n", __func__);
+	return cdc_enc_killurb(wwan->cdc_enc);
+}
+
+int qmi_disconnect(struct cdc_enc_client *wwan)
+{
+	pr_debug("%s()\n", __func__);
+	qmi_wds_stop(wwan);
+	clear_bit(QMI_WWAN_UP, &wwan->flags);
+	set_bit(QMI_WWAN_DISABLE, &wwan->flags);
+	return 1;
+}
+
+static const struct cdc_enc_protocol qmi_proto = {
+	.name = "QMI",
+	.demux_register = qmi_demux_register,
+	.demux_lookup = qmi_demux_lookup,
+};
+
+/* allocate and initiate a QMI device with a client */
+struct cdc_enc_client *qmi_wwan_client_init(struct usb_interface *intf)
+{
+	struct cdc_enc_client *wwan = NULL;
+	struct cdc_enc_state *cdc_enc = cdc_enc_init_one(intf, &qmi_proto);
+
+	if (!cdc_enc)
+		goto err;
+
+	/* add the wwan client */
+	wwan = cdc_enc_add_client(cdc_enc);
+	if (!wwan)
+		cdc_enc_free_one(cdc_enc);
+
+err:
+	return wwan;
+}
+
+int qmi_wwan_client_exit(struct cdc_enc_client *wwan)
+{
+	struct cdc_enc_state *cdc_enc = wwan->cdc_enc;
+
+	qmi_shutdown(wwan);
+	cdc_enc_destroy_client(wwan);
+	return cdc_enc_free_one(cdc_enc);
+}
diff --git a/drivers/net/usb/qmi_proto.h b/drivers/net/usb/qmi_proto.h
new file mode 100644
index 0000000..d06c56d
--- /dev/null
+++ b/drivers/net/usb/qmi_proto.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2012  Bjørn Mork <bjorn@xxxxxxx>
+ * Copyright (c) 2010, Code Aurora Forum. All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ */
+
+/*
+ * Dealing with devices using Qualcomm MSM Interface (QMI) for
+ * configuration.  Full documentation of the protocol can be obtained
+ * from http://developer.qualcomm.com/
+ *
+ * Some of this code may be inspired by code from the Qualcomm Gobi
+ * 2000 and Gobi 3000 drivers, available at http://www.codeaurora.org/
+ */
+
+/* the different QMI subsystems */
+enum qmi_subsystems {
+	QMI_CTL = 0,
+	QMI_WDS,
+	QMI_DMS, 
+	QMI_NAS, 
+	QMI_QOS,
+	QMI_WMS,
+	QMI_PDS,
+	QMI_VOICE = 0x09,
+	QMI_CAT, 
+	QMI_UIM,
+	QMI_PBM,
+};
+
+/* putting state information in private flags */
+enum qmi_wwan_state_flag {
+	QMI_WWAN_DISABLE = CDC_ENC_CLIENT_PRIVATE,	/* disable connecting */
+	QMI_WWAN_UP,					/* connection is up */
+};
+
+/* QMI protocol structures */
+struct qmux_header {
+	u8 tf;		/* always 1 */
+	__le16 len;	/* excluding tf */
+	u8 ctrl;	/* b7: sendertype 1 => service, 0 => control point */
+	u8 service;	/* 0 => QMI_CTL, 1 => QMI_WDS, .. */
+	u8 qmicid;	/* client id or 0xff for broadcast */
+	u8 flags;	/* always 0 for req */
+} __packed;
+
+struct qmi_msg {
+	__le16 msgid;
+	__le16 len;
+	u8 tlv[];	/* zero or more tlvs */
+} __packed;
+
+struct qmi_ctl {
+	struct qmux_header h;
+	u8 tid;	/* system QMI_CTL uses one byte transaction ids! */
+	struct qmi_msg m;
+} __packed;
+
+struct qmi_wds {
+	struct qmux_header h;
+	__le16 tid;
+	struct qmi_msg m;
+} __packed;
+
+struct qmi_tlv {
+	u8 type;
+	__le16 len;
+	u8 bytes[];
+} __packed;
+
+struct qmi_tlv_response_data {
+	__le16 error;
+	__le16 code;
+} __packed;
+
+/* reset state to INIT */
+extern int qmi_reset(struct cdc_enc_client *wwan);
+extern int qmi_suspend(struct cdc_enc_client *wwan);
+
+/* disconnect WWAN connection */
+extern int qmi_disconnect(struct cdc_enc_client *wwan);
+
+/* call this whenever a state transition is wanted */
+extern int qmi_conn_state(struct cdc_enc_client *wwan);
+
+/* initialize a QMI client */
+extern struct cdc_enc_client *qmi_wwan_client_init(struct usb_interface *intf);
+
+/* destroy a QMI client */
+extern int qmi_wwan_client_exit(struct cdc_enc_client *wwan);
diff --git a/drivers/net/usb/qmi_wwan_core.c b/drivers/net/usb/qmi_wwan_core.c
new file mode 100644
index 0000000..4ff1feb
--- /dev/null
+++ b/drivers/net/usb/qmi_wwan_core.c
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2012  Bjørn Mork <bjorn@xxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ */
+
+#include <linux/module.h>
+#include <linux/cdev.h>
+#include <linux/netdevice.h>
+#include <linux/ethtool.h>
+#include <linux/mii.h>
+#include <linux/usb.h>
+#include <linux/usb/cdc.h>
+#include <linux/usb/usbnet.h>
+#include "cdc_enc.h"
+#include "qmi_proto.h"
+
+/* overloading the cdc_state structure */
+struct qmi_wwan_state {
+	struct cdc_enc_client		*wwan;
+	struct usb_cdc_union_desc	*u;
+	struct usb_cdc_ether_desc	*ether;
+	struct usb_interface		*control;
+	struct usb_interface		*data;
+};
+
+static void qmi_wwan_cdc_status(struct usbnet *dev, struct urb *urb)
+{
+	struct usb_cdc_notification	*event;
+	struct qmi_wwan_state *info = (void *)&dev->data;
+
+	if (urb->actual_length < sizeof *event) {
+		netdev_dbg(dev->net, "short status urb rcvd: %d bytes\n", urb->actual_length);
+		return;
+	}
+
+	event = urb->transfer_buffer;
+	switch (event->bNotificationType) {
+	case USB_CDC_NOTIFY_RESPONSE_AVAILABLE:
+		cdc_enc_recv(info->wwan->cdc_enc);	/* request response */
+		break;
+	default:
+		/* reuse usbnet() for handling other messages */
+		usbnet_cdc_status(dev, urb);
+	}
+}
+
+static int qmi_wwan_cdc_ecmlike_bind(struct usbnet *dev, struct usb_interface *intf)
+{
+	int status;
+	struct qmi_wwan_state *info = (void *)&dev->data;
+
+	status = usbnet_cdc_bind(dev, intf);
+	if (status < 0)
+		return status;
+
+	/* initialize QMI data */
+	info->wwan = qmi_wwan_client_init(intf);
+	if (!info->wwan) {
+		usbnet_cdc_unbind(dev, intf);
+		return -1;
+	}
+	return 0;
+}
+
+static int qmi_wwan_cdc_bind(struct usbnet *dev, struct usb_interface *intf)
+{
+	int status;
+	struct qmi_wwan_state *info = (void *)&dev->data;
+
+	memset(info, 0, sizeof(*info));
+	status = usbnet_get_endpoints(dev, intf);
+	if (status < 0)
+		return status;
+
+	/* initialize QMI data */
+	info->wwan = qmi_wwan_client_init(intf);
+	if (!info->wwan)
+		return -1;
+
+	info->control = intf;
+	return 0;
+}
+
+static void qmi_wwan_cdc_unbind(struct usbnet *dev, struct usb_interface *intf)
+{
+	struct qmi_wwan_state *info = (void *)&dev->data;
+
+	/* release our private structure */
+	qmi_wwan_client_exit(info->wwan);
+	info->wwan = NULL;
+
+	/* disconnect */
+	usbnet_cdc_unbind(dev, intf);
+}
+
+static int qmi_wwan_reset(struct usbnet *dev)
+{
+	struct qmi_wwan_state *info = (void *)&dev->data;
+
+	return qmi_reset(info->wwan);
+}
+
+static int qmi_wwan_stop(struct usbnet *dev)
+{
+	struct qmi_wwan_state *info = (void *)&dev->data;
+
+	return qmi_disconnect(info->wwan);
+}
+
+static int qmi_wwan_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct usbnet *dev = usb_get_intfdata(intf);
+	struct qmi_wwan_state *info = (void *)&dev->data;
+	int ret;
+
+	ret = qmi_suspend(info->wwan);
+	if (ret == 0)
+		ret = usbnet_suspend(intf, message);
+
+	return ret;
+}
+
+static int qmi_wwan_manage_power(struct usbnet *dev, int on)
+{
+	dev->intf->needs_remote_wakeup = on;
+	return 0;
+}
+
+/* abusing check_connect for triggering QMI state transitions */
+static int qmi_wwan_check_connect(struct usbnet *dev)
+{
+	struct qmi_wwan_state *info = (void *)&dev->data;
+
+	return qmi_conn_state(info->wwan);
+}
+
+static const struct driver_info	qmi_wwan_ecm_info = {
+	.description =	"QMI speaking CDC ECM like wwan device",
+	.flags =	 FLAG_WWAN,
+	.bind =		 qmi_wwan_cdc_ecmlike_bind,
+	.unbind =	 qmi_wwan_cdc_unbind,
+	.status =	 qmi_wwan_cdc_status,
+	.manage_power =	qmi_wwan_manage_power,
+	.check_connect = qmi_wwan_check_connect,
+	.stop =          qmi_wwan_stop,
+	.reset =         qmi_wwan_reset,
+};
+
+static const struct driver_info	qmi_wwan_info = {
+	.description =	"QMI speaking wwan device",
+	.flags =	 FLAG_WWAN,
+	.bind =		 qmi_wwan_cdc_bind,
+	.unbind =	 qmi_wwan_cdc_unbind,
+	.status =	 qmi_wwan_cdc_status,
+	.manage_power =	qmi_wwan_manage_power,
+	.check_connect = qmi_wwan_check_connect,
+	.stop =          qmi_wwan_stop,
+	.reset =         qmi_wwan_reset,
+};
+
+#define HUAWEI_VENDOR_ID	0x12D1
+
+static const struct usb_device_id products[] = {
+{
+	/* Qmi_Wwan E392, E398, ++? */
+	.match_flags    =   USB_DEVICE_ID_MATCH_VENDOR
+		 | USB_DEVICE_ID_MATCH_INT_INFO,
+	.idVendor               = HUAWEI_VENDOR_ID,
+	.bInterfaceClass	= USB_CLASS_VENDOR_SPEC,
+	.bInterfaceSubClass	= 1,
+	.bInterfaceProtocol	= 9,
+	.driver_info = (unsigned long)&qmi_wwan_ecm_info,
+}, {
+	/* Qmi_Wwan device id 1413 ++? */
+	.match_flags    =   USB_DEVICE_ID_MATCH_VENDOR
+		 | USB_DEVICE_ID_MATCH_INT_INFO,
+	.idVendor               = HUAWEI_VENDOR_ID,
+	.bInterfaceClass	= USB_CLASS_VENDOR_SPEC,
+	.bInterfaceSubClass	= 6,
+	.bInterfaceProtocol	= 255,
+	.driver_info = (unsigned long)&qmi_wwan_ecm_info,
+}, {
+	/* Qmi_Wwan E392, E398, ++? "Windows mode" */
+	.match_flags    =   USB_DEVICE_ID_MATCH_VENDOR
+		 | USB_DEVICE_ID_MATCH_INT_INFO,
+	.idVendor               = HUAWEI_VENDOR_ID,
+	.bInterfaceClass	= USB_CLASS_VENDOR_SPEC,
+	.bInterfaceSubClass	= 1,
+	.bInterfaceProtocol	= 17,
+	.driver_info = (unsigned long)&qmi_wwan_info,
+},
+{ },		/* END */
+};
+MODULE_DEVICE_TABLE(usb, products);
+
+static struct usb_driver qmi_wwan_driver = {
+	.name =		"qmi_wwan",
+	.id_table =	products,
+	.probe =	usbnet_probe,
+	.disconnect =	usbnet_disconnect,
+	.suspend =	qmi_wwan_suspend,
+	.resume =	usbnet_resume,
+	.reset_resume =	usbnet_resume,
+	.supports_autosuspend = 1,
+};
+
+static int __init qmi_wwan_init(void)
+{
+	/* we remap struct (cdc_state) so we should be compatible */
+	BUILD_BUG_ON(sizeof(struct cdc_state) != sizeof(struct qmi_wwan_state));
+
+	return usb_register(&qmi_wwan_driver);
+}
+module_init(qmi_wwan_init);
+
+static void __exit qmi_wwan_exit(void)
+{
+	usb_deregister(&qmi_wwan_driver);
+}
+module_exit(qmi_wwan_exit);
+
+MODULE_AUTHOR("Bjørn Mork <bjorn@xxxxxxx>");
+MODULE_DESCRIPTION("Qualcomm MSM Interface (QMI) WWAN driver");
+MODULE_LICENSE("GPL");

[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux