| /* arch/arm/mach-msm/smd_qmi.c |
| * |
| * QMI Control Driver -- Manages network data connections. |
| * |
| * Copyright (C) 2007 Google, Inc. |
| * Author: Brian Swetland <swetland@google.com> |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/fs.h> |
| #include <linux/cdev.h> |
| #include <linux/device.h> |
| #include <linux/sched.h> |
| #include <linux/wait.h> |
| #include <linux/miscdevice.h> |
| #include <linux/workqueue.h> |
| #include <linux/wakelock.h> |
| |
| #include <asm/uaccess.h> |
| #include <mach/msm_smd.h> |
| |
| #define QMI_CTL 0x00 |
| #define QMI_WDS 0x01 |
| #define QMI_DMS 0x02 |
| #define QMI_NAS 0x03 |
| |
| #define QMI_RESULT_SUCCESS 0x0000 |
| #define QMI_RESULT_FAILURE 0x0001 |
| |
| struct qmi_msg { |
| unsigned char service; |
| unsigned char client_id; |
| unsigned short txn_id; |
| unsigned short type; |
| unsigned short size; |
| unsigned char *tlv; |
| }; |
| |
| #define qmi_ctl_client_id 0 |
| |
| #define STATE_OFFLINE 0 |
| #define STATE_QUERYING 1 |
| #define STATE_ONLINE 2 |
| |
| struct qmi_ctxt { |
| struct miscdevice misc; |
| |
| struct mutex lock; |
| |
| unsigned char ctl_txn_id; |
| unsigned char wds_client_id; |
| unsigned short wds_txn_id; |
| |
| unsigned wds_busy; |
| unsigned wds_handle; |
| unsigned state_dirty; |
| unsigned state; |
| |
| unsigned char addr[4]; |
| unsigned char mask[4]; |
| unsigned char gateway[4]; |
| unsigned char dns1[4]; |
| unsigned char dns2[4]; |
| |
| smd_channel_t *ch; |
| const char *ch_name; |
| struct wake_lock wake_lock; |
| |
| struct work_struct open_work; |
| struct work_struct read_work; |
| }; |
| |
| static struct qmi_ctxt *qmi_minor_to_ctxt(unsigned n); |
| |
| static void qmi_read_work(struct work_struct *ws); |
| static void qmi_open_work(struct work_struct *work); |
| |
| void qmi_ctxt_init(struct qmi_ctxt *ctxt, unsigned n) |
| { |
| mutex_init(&ctxt->lock); |
| INIT_WORK(&ctxt->read_work, qmi_read_work); |
| INIT_WORK(&ctxt->open_work, qmi_open_work); |
| wake_lock_init(&ctxt->wake_lock, WAKE_LOCK_SUSPEND, ctxt->misc.name); |
| ctxt->ctl_txn_id = 1; |
| ctxt->wds_txn_id = 1; |
| ctxt->wds_busy = 1; |
| ctxt->state = STATE_OFFLINE; |
| |
| } |
| |
| static struct workqueue_struct *qmi_wq; |
| |
| static int verbose = 0; |
| |
| /* anyone waiting for a state change waits here */ |
| static DECLARE_WAIT_QUEUE_HEAD(qmi_wait_queue); |
| |
| |
| static void qmi_dump_msg(struct qmi_msg *msg, const char *prefix) |
| { |
| unsigned sz, n; |
| unsigned char *x; |
| |
| if (!verbose) |
| return; |
| |
| printk(KERN_INFO |
| "qmi: %s: svc=%02x cid=%02x tid=%04x type=%04x size=%04x\n", |
| prefix, msg->service, msg->client_id, |
| msg->txn_id, msg->type, msg->size); |
| |
| x = msg->tlv; |
| sz = msg->size; |
| |
| while (sz >= 3) { |
| sz -= 3; |
| |
| n = x[1] | (x[2] << 8); |
| if (n > sz) |
| break; |
| |
| printk(KERN_INFO "qmi: %s: tlv: %02x %04x { ", |
| prefix, x[0], n); |
| x += 3; |
| sz -= n; |
| while (n-- > 0) |
| printk("%02x ", *x++); |
| printk("}\n"); |
| } |
| } |
| |
| int qmi_add_tlv(struct qmi_msg *msg, |
| unsigned type, unsigned size, const void *data) |
| { |
| unsigned char *x = msg->tlv + msg->size; |
| |
| x[0] = type; |
| x[1] = size; |
| x[2] = size >> 8; |
| |
| memcpy(x + 3, data, size); |
| |
| msg->size += (size + 3); |
| |
| return 0; |
| } |
| |
| /* Extract a tagged item from a qmi message buffer, |
| ** taking care not to overrun the buffer. |
| */ |
| static int qmi_get_tlv(struct qmi_msg *msg, |
| unsigned type, unsigned size, void *data) |
| { |
| unsigned char *x = msg->tlv; |
| unsigned len = msg->size; |
| unsigned n; |
| |
| while (len >= 3) { |
| len -= 3; |
| |
| /* size of this item */ |
| n = x[1] | (x[2] << 8); |
| if (n > len) |
| break; |
| |
| if (x[0] == type) { |
| if (n != size) |
| return -1; |
| memcpy(data, x + 3, size); |
| return 0; |
| } |
| |
| x += (n + 3); |
| len -= n; |
| } |
| |
| return -1; |
| } |
| |
| static unsigned qmi_get_status(struct qmi_msg *msg, unsigned *error) |
| { |
| unsigned short status[2]; |
| if (qmi_get_tlv(msg, 0x02, sizeof(status), status)) { |
| *error = 0; |
| return QMI_RESULT_FAILURE; |
| } else { |
| *error = status[1]; |
| return status[0]; |
| } |
| } |
| |
| /* 0x01 <qmux-header> <payload> */ |
| #define QMUX_HEADER 13 |
| |
| /* should be >= HEADER + FOOTER */ |
| #define QMUX_OVERHEAD 16 |
| |
| static int qmi_send(struct qmi_ctxt *ctxt, struct qmi_msg *msg) |
| { |
| unsigned char *data; |
| unsigned hlen; |
| unsigned len; |
| int r; |
| |
| qmi_dump_msg(msg, "send"); |
| |
| if (msg->service == QMI_CTL) { |
| hlen = QMUX_HEADER - 1; |
| } else { |
| hlen = QMUX_HEADER; |
| } |
| |
| /* QMUX length is total header + total payload - IFC selector */ |
| len = hlen + msg->size - 1; |
| if (len > 0xffff) |
| return -1; |
| |
| data = msg->tlv - hlen; |
| |
| /* prepend encap and qmux header */ |
| *data++ = 0x01; /* ifc selector */ |
| |
| /* qmux header */ |
| *data++ = len; |
| *data++ = len >> 8; |
| *data++ = 0x00; /* flags: client */ |
| *data++ = msg->service; |
| *data++ = msg->client_id; |
| |
| /* qmi header */ |
| *data++ = 0x00; /* flags: send */ |
| *data++ = msg->txn_id; |
| if (msg->service != QMI_CTL) |
| *data++ = msg->txn_id >> 8; |
| |
| *data++ = msg->type; |
| *data++ = msg->type >> 8; |
| *data++ = msg->size; |
| *data++ = msg->size >> 8; |
| |
| /* len + 1 takes the interface selector into account */ |
| r = smd_write(ctxt->ch, msg->tlv - hlen, len + 1); |
| |
| if (r != len) { |
| return -1; |
| } else { |
| return 0; |
| } |
| } |
| |
| static void qmi_process_ctl_msg(struct qmi_ctxt *ctxt, struct qmi_msg *msg) |
| { |
| unsigned err; |
| if (msg->type == 0x0022) { |
| unsigned char n[2]; |
| if (qmi_get_status(msg, &err)) |
| return; |
| if (qmi_get_tlv(msg, 0x01, sizeof(n), n)) |
| return; |
| if (n[0] == QMI_WDS) { |
| printk(KERN_INFO |
| "qmi: ctl: wds use client_id 0x%02x\n", n[1]); |
| ctxt->wds_client_id = n[1]; |
| ctxt->wds_busy = 0; |
| } |
| } |
| } |
| |
| static int qmi_network_get_profile(struct qmi_ctxt *ctxt); |
| |
| static void swapaddr(unsigned char *src, unsigned char *dst) |
| { |
| dst[0] = src[3]; |
| dst[1] = src[2]; |
| dst[2] = src[1]; |
| dst[3] = src[0]; |
| } |
| |
| static unsigned char zero[4]; |
| static void qmi_read_runtime_profile(struct qmi_ctxt *ctxt, struct qmi_msg *msg) |
| { |
| unsigned char tmp[4]; |
| unsigned r; |
| |
| r = qmi_get_tlv(msg, 0x1e, 4, tmp); |
| swapaddr(r ? zero : tmp, ctxt->addr); |
| r = qmi_get_tlv(msg, 0x21, 4, tmp); |
| swapaddr(r ? zero : tmp, ctxt->mask); |
| r = qmi_get_tlv(msg, 0x20, 4, tmp); |
| swapaddr(r ? zero : tmp, ctxt->gateway); |
| r = qmi_get_tlv(msg, 0x15, 4, tmp); |
| swapaddr(r ? zero : tmp, ctxt->dns1); |
| r = qmi_get_tlv(msg, 0x16, 4, tmp); |
| swapaddr(r ? zero : tmp, ctxt->dns2); |
| } |
| |
| static void qmi_process_unicast_wds_msg(struct qmi_ctxt *ctxt, |
| struct qmi_msg *msg) |
| { |
| unsigned err; |
| switch (msg->type) { |
| case 0x0021: |
| if (qmi_get_status(msg, &err)) { |
| printk(KERN_ERR |
| "qmi: wds: network stop failed (%04x)\n", err); |
| } else { |
| printk(KERN_INFO |
| "qmi: wds: network stopped\n"); |
| ctxt->state = STATE_OFFLINE; |
| ctxt->state_dirty = 1; |
| } |
| break; |
| case 0x0020: |
| if (qmi_get_status(msg, &err)) { |
| printk(KERN_ERR |
| "qmi: wds: network start failed (%04x)\n", err); |
| } else if (qmi_get_tlv(msg, 0x01, sizeof(ctxt->wds_handle), &ctxt->wds_handle)) { |
| printk(KERN_INFO |
| "qmi: wds no handle?\n"); |
| } else { |
| printk(KERN_INFO |
| "qmi: wds: got handle 0x%08x\n", |
| ctxt->wds_handle); |
| } |
| break; |
| case 0x002D: |
| printk("qmi: got network profile\n"); |
| if (ctxt->state == STATE_QUERYING) { |
| qmi_read_runtime_profile(ctxt, msg); |
| ctxt->state = STATE_ONLINE; |
| ctxt->state_dirty = 1; |
| } |
| break; |
| default: |
| printk(KERN_ERR "qmi: unknown msg type 0x%04x\n", msg->type); |
| } |
| ctxt->wds_busy = 0; |
| } |
| |
| static void qmi_process_broadcast_wds_msg(struct qmi_ctxt *ctxt, |
| struct qmi_msg *msg) |
| { |
| if (msg->type == 0x0022) { |
| unsigned char n[2]; |
| if (qmi_get_tlv(msg, 0x01, sizeof(n), n)) |
| return; |
| switch (n[0]) { |
| case 1: |
| printk(KERN_INFO "qmi: wds: DISCONNECTED\n"); |
| ctxt->state = STATE_OFFLINE; |
| ctxt->state_dirty = 1; |
| break; |
| case 2: |
| printk(KERN_INFO "qmi: wds: CONNECTED\n"); |
| ctxt->state = STATE_QUERYING; |
| ctxt->state_dirty = 1; |
| qmi_network_get_profile(ctxt); |
| break; |
| case 3: |
| printk(KERN_INFO "qmi: wds: SUSPENDED\n"); |
| ctxt->state = STATE_OFFLINE; |
| ctxt->state_dirty = 1; |
| } |
| } else { |
| printk(KERN_ERR "qmi: unknown bcast msg type 0x%04x\n", msg->type); |
| } |
| } |
| |
| static void qmi_process_wds_msg(struct qmi_ctxt *ctxt, |
| struct qmi_msg *msg) |
| { |
| printk("wds: %04x @ %02x\n", msg->type, msg->client_id); |
| if (msg->client_id == ctxt->wds_client_id) { |
| qmi_process_unicast_wds_msg(ctxt, msg); |
| } else if (msg->client_id == 0xff) { |
| qmi_process_broadcast_wds_msg(ctxt, msg); |
| } else { |
| printk(KERN_ERR |
| "qmi_process_wds_msg client id 0x%02x unknown\n", |
| msg->client_id); |
| } |
| } |
| |
| static void qmi_process_qmux(struct qmi_ctxt *ctxt, |
| unsigned char *buf, unsigned sz) |
| { |
| struct qmi_msg msg; |
| |
| /* require a full header */ |
| if (sz < 5) |
| return; |
| |
| /* require a size that matches the buffer size */ |
| if (sz != (buf[0] | (buf[1] << 8))) |
| return; |
| |
| /* only messages from a service (bit7=1) are allowed */ |
| if (buf[2] != 0x80) |
| return; |
| |
| msg.service = buf[3]; |
| msg.client_id = buf[4]; |
| |
| /* annoyingly, CTL messages have a shorter TID */ |
| if (buf[3] == 0) { |
| if (sz < 7) |
| return; |
| msg.txn_id = buf[6]; |
| buf += 7; |
| sz -= 7; |
| } else { |
| if (sz < 8) |
| return; |
| msg.txn_id = buf[6] | (buf[7] << 8); |
| buf += 8; |
| sz -= 8; |
| } |
| |
| /* no type and size!? */ |
| if (sz < 4) |
| return; |
| sz -= 4; |
| |
| msg.type = buf[0] | (buf[1] << 8); |
| msg.size = buf[2] | (buf[3] << 8); |
| msg.tlv = buf + 4; |
| |
| if (sz != msg.size) |
| return; |
| |
| qmi_dump_msg(&msg, "recv"); |
| |
| mutex_lock(&ctxt->lock); |
| switch (msg.service) { |
| case QMI_CTL: |
| qmi_process_ctl_msg(ctxt, &msg); |
| break; |
| case QMI_WDS: |
| qmi_process_wds_msg(ctxt, &msg); |
| break; |
| default: |
| printk(KERN_ERR "qmi: msg from unknown svc 0x%02x\n", |
| msg.service); |
| break; |
| } |
| mutex_unlock(&ctxt->lock); |
| |
| wake_up(&qmi_wait_queue); |
| } |
| |
| #define QMI_MAX_PACKET (256 + QMUX_OVERHEAD) |
| |
| static void qmi_read_work(struct work_struct *ws) |
| { |
| struct qmi_ctxt *ctxt = container_of(ws, struct qmi_ctxt, read_work); |
| struct smd_channel *ch = ctxt->ch; |
| unsigned char buf[QMI_MAX_PACKET]; |
| int sz; |
| |
| for (;;) { |
| sz = smd_cur_packet_size(ch); |
| if (sz == 0) |
| break; |
| if (sz < smd_read_avail(ch)) |
| break; |
| if (sz > QMI_MAX_PACKET) { |
| smd_read(ch, 0, sz); |
| continue; |
| } |
| if (smd_read(ch, buf, sz) != sz) { |
| printk(KERN_ERR "qmi: not enough data?!\n"); |
| continue; |
| } |
| |
| /* interface selector must be 1 */ |
| if (buf[0] != 0x01) |
| continue; |
| |
| qmi_process_qmux(ctxt, buf + 1, sz - 1); |
| } |
| } |
| |
| static int qmi_request_wds_cid(struct qmi_ctxt *ctxt); |
| |
| static void qmi_open_work(struct work_struct *ws) |
| { |
| struct qmi_ctxt *ctxt = container_of(ws, struct qmi_ctxt, open_work); |
| mutex_lock(&ctxt->lock); |
| qmi_request_wds_cid(ctxt); |
| mutex_unlock(&ctxt->lock); |
| } |
| |
| static void qmi_notify(void *priv, unsigned event) |
| { |
| struct qmi_ctxt *ctxt = priv; |
| |
| switch (event) { |
| case SMD_EVENT_DATA: { |
| int sz; |
| sz = smd_cur_packet_size(ctxt->ch); |
| if ((sz > 0) && (sz <= smd_read_avail(ctxt->ch))) { |
| wake_lock_timeout(&ctxt->wake_lock, HZ / 2); |
| queue_work(qmi_wq, &ctxt->read_work); |
| } |
| break; |
| } |
| case SMD_EVENT_OPEN: |
| printk(KERN_INFO "qmi: smd opened\n"); |
| queue_work(qmi_wq, &ctxt->open_work); |
| break; |
| case SMD_EVENT_CLOSE: |
| printk(KERN_INFO "qmi: smd closed\n"); |
| break; |
| } |
| } |
| |
| static int qmi_request_wds_cid(struct qmi_ctxt *ctxt) |
| { |
| unsigned char data[64 + QMUX_OVERHEAD]; |
| struct qmi_msg msg; |
| unsigned char n; |
| |
| msg.service = QMI_CTL; |
| msg.client_id = qmi_ctl_client_id; |
| msg.txn_id = ctxt->ctl_txn_id; |
| msg.type = 0x0022; |
| msg.size = 0; |
| msg.tlv = data + QMUX_HEADER; |
| |
| ctxt->ctl_txn_id += 2; |
| |
| n = QMI_WDS; |
| qmi_add_tlv(&msg, 0x01, 0x01, &n); |
| |
| return qmi_send(ctxt, &msg); |
| } |
| |
| static int qmi_network_get_profile(struct qmi_ctxt *ctxt) |
| { |
| unsigned char data[96 + QMUX_OVERHEAD]; |
| struct qmi_msg msg; |
| |
| msg.service = QMI_WDS; |
| msg.client_id = ctxt->wds_client_id; |
| msg.txn_id = ctxt->wds_txn_id; |
| msg.type = 0x002D; |
| msg.size = 0; |
| msg.tlv = data + QMUX_HEADER; |
| |
| ctxt->wds_txn_id += 2; |
| |
| return qmi_send(ctxt, &msg); |
| } |
| |
| static int qmi_network_up(struct qmi_ctxt *ctxt, char *apn) |
| { |
| unsigned char data[96 + QMUX_OVERHEAD]; |
| struct qmi_msg msg; |
| char *user; |
| char *pass; |
| |
| for (user = apn; *user; user++) { |
| if (*user == ' ') { |
| *user++ = 0; |
| break; |
| } |
| } |
| for (pass = user; *pass; pass++) { |
| if (*pass == ' ') { |
| *pass++ = 0; |
| break; |
| } |
| } |
| |
| msg.service = QMI_WDS; |
| msg.client_id = ctxt->wds_client_id; |
| msg.txn_id = ctxt->wds_txn_id; |
| msg.type = 0x0020; |
| msg.size = 0; |
| msg.tlv = data + QMUX_HEADER; |
| |
| ctxt->wds_txn_id += 2; |
| |
| qmi_add_tlv(&msg, 0x14, strlen(apn), apn); |
| if (*user) { |
| unsigned char x; |
| x = 3; |
| qmi_add_tlv(&msg, 0x16, 1, &x); |
| qmi_add_tlv(&msg, 0x17, strlen(user), user); |
| if (*pass) |
| qmi_add_tlv(&msg, 0x18, strlen(pass), pass); |
| } |
| return qmi_send(ctxt, &msg); |
| } |
| |
| static int qmi_network_down(struct qmi_ctxt *ctxt) |
| { |
| unsigned char data[16 + QMUX_OVERHEAD]; |
| struct qmi_msg msg; |
| |
| msg.service = QMI_WDS; |
| msg.client_id = ctxt->wds_client_id; |
| msg.txn_id = ctxt->wds_txn_id; |
| msg.type = 0x0021; |
| msg.size = 0; |
| msg.tlv = data + QMUX_HEADER; |
| |
| ctxt->wds_txn_id += 2; |
| |
| qmi_add_tlv(&msg, 0x01, sizeof(ctxt->wds_handle), &ctxt->wds_handle); |
| |
| return qmi_send(ctxt, &msg); |
| } |
| |
| static int qmi_print_state(struct qmi_ctxt *ctxt, char *buf, int max) |
| { |
| int i; |
| char *statename; |
| |
| if (ctxt->state == STATE_ONLINE) { |
| statename = "up"; |
| } else if (ctxt->state == STATE_OFFLINE) { |
| statename = "down"; |
| } else { |
| statename = "busy"; |
| } |
| |
| i = scnprintf(buf, max, "STATE=%s\n", statename); |
| i += scnprintf(buf + i, max - i, "CID=%d\n",ctxt->wds_client_id); |
| |
| if (ctxt->state != STATE_ONLINE){ |
| return i; |
| } |
| |
| i += scnprintf(buf + i, max - i, "ADDR=%d.%d.%d.%d\n", |
| ctxt->addr[0], ctxt->addr[1], ctxt->addr[2], ctxt->addr[3]); |
| i += scnprintf(buf + i, max - i, "MASK=%d.%d.%d.%d\n", |
| ctxt->mask[0], ctxt->mask[1], ctxt->mask[2], ctxt->mask[3]); |
| i += scnprintf(buf + i, max - i, "GATEWAY=%d.%d.%d.%d\n", |
| ctxt->gateway[0], ctxt->gateway[1], ctxt->gateway[2], |
| ctxt->gateway[3]); |
| i += scnprintf(buf + i, max - i, "DNS1=%d.%d.%d.%d\n", |
| ctxt->dns1[0], ctxt->dns1[1], ctxt->dns1[2], ctxt->dns1[3]); |
| i += scnprintf(buf + i, max - i, "DNS2=%d.%d.%d.%d\n", |
| ctxt->dns2[0], ctxt->dns2[1], ctxt->dns2[2], ctxt->dns2[3]); |
| |
| return i; |
| } |
| |
| static ssize_t qmi_read(struct file *fp, char __user *buf, |
| size_t count, loff_t *pos) |
| { |
| struct qmi_ctxt *ctxt = fp->private_data; |
| char msg[256]; |
| int len; |
| int r; |
| |
| mutex_lock(&ctxt->lock); |
| for (;;) { |
| if (ctxt->state_dirty) { |
| ctxt->state_dirty = 0; |
| len = qmi_print_state(ctxt, msg, 256); |
| break; |
| } |
| mutex_unlock(&ctxt->lock); |
| r = wait_event_interruptible(qmi_wait_queue, ctxt->state_dirty); |
| if (r < 0) |
| return r; |
| mutex_lock(&ctxt->lock); |
| } |
| mutex_unlock(&ctxt->lock); |
| |
| if (len > count) |
| len = count; |
| |
| if (copy_to_user(buf, msg, len)) |
| return -EFAULT; |
| |
| return len; |
| } |
| |
| |
| static ssize_t qmi_write(struct file *fp, const char __user *buf, |
| size_t count, loff_t *pos) |
| { |
| struct qmi_ctxt *ctxt = fp->private_data; |
| unsigned char cmd[64]; |
| int len; |
| int r; |
| |
| if (count < 1) |
| return 0; |
| |
| len = count > 63 ? 63 : count; |
| |
| if (copy_from_user(cmd, buf, len)) |
| return -EFAULT; |
| |
| cmd[len] = 0; |
| |
| /* lazy */ |
| if (cmd[len-1] == '\n') { |
| cmd[len-1] = 0; |
| len--; |
| } |
| |
| if (!strncmp(cmd, "verbose", 7)) { |
| verbose = 1; |
| } else if (!strncmp(cmd, "terse", 5)) { |
| verbose = 0; |
| } else if (!strncmp(cmd, "poll", 4)) { |
| ctxt->state_dirty = 1; |
| wake_up(&qmi_wait_queue); |
| } else if (!strncmp(cmd, "down", 4)) { |
| retry_down: |
| mutex_lock(&ctxt->lock); |
| if (ctxt->wds_busy) { |
| mutex_unlock(&ctxt->lock); |
| r = wait_event_interruptible(qmi_wait_queue, !ctxt->wds_busy); |
| if (r < 0) |
| return r; |
| goto retry_down; |
| } |
| ctxt->wds_busy = 1; |
| qmi_network_down(ctxt); |
| mutex_unlock(&ctxt->lock); |
| } else if (!strncmp(cmd, "up:", 3)) { |
| retry_up: |
| mutex_lock(&ctxt->lock); |
| if (ctxt->wds_busy) { |
| mutex_unlock(&ctxt->lock); |
| r = wait_event_interruptible(qmi_wait_queue, !ctxt->wds_busy); |
| if (r < 0) |
| return r; |
| goto retry_up; |
| } |
| ctxt->wds_busy = 1; |
| qmi_network_up(ctxt, cmd+3); |
| mutex_unlock(&ctxt->lock); |
| } else { |
| return -EINVAL; |
| } |
| |
| return count; |
| } |
| |
| static int qmi_open(struct inode *ip, struct file *fp) |
| { |
| struct qmi_ctxt *ctxt = qmi_minor_to_ctxt(MINOR(ip->i_rdev)); |
| int r = 0; |
| |
| if (!ctxt) { |
| printk(KERN_ERR "unknown qmi misc %d\n", MINOR(ip->i_rdev)); |
| return -ENODEV; |
| } |
| |
| fp->private_data = ctxt; |
| |
| mutex_lock(&ctxt->lock); |
| if (ctxt->ch == 0) |
| r = smd_open(ctxt->ch_name, &ctxt->ch, ctxt, qmi_notify); |
| if (r == 0) |
| wake_up(&qmi_wait_queue); |
| mutex_unlock(&ctxt->lock); |
| |
| return r; |
| } |
| |
| static int qmi_release(struct inode *ip, struct file *fp) |
| { |
| return 0; |
| } |
| |
| static struct file_operations qmi_fops = { |
| .owner = THIS_MODULE, |
| .read = qmi_read, |
| .write = qmi_write, |
| .open = qmi_open, |
| .release = qmi_release, |
| }; |
| |
| static struct qmi_ctxt qmi_device0 = { |
| .ch_name = "SMD_DATA5_CNTL", |
| .misc = { |
| .minor = MISC_DYNAMIC_MINOR, |
| .name = "qmi0", |
| .fops = &qmi_fops, |
| } |
| }; |
| static struct qmi_ctxt qmi_device1 = { |
| .ch_name = "SMD_DATA6_CNTL", |
| .misc = { |
| .minor = MISC_DYNAMIC_MINOR, |
| .name = "qmi1", |
| .fops = &qmi_fops, |
| } |
| }; |
| static struct qmi_ctxt qmi_device2 = { |
| .ch_name = "SMD_DATA7_CNTL", |
| .misc = { |
| .minor = MISC_DYNAMIC_MINOR, |
| .name = "qmi2", |
| .fops = &qmi_fops, |
| } |
| }; |
| |
| static struct qmi_ctxt *qmi_minor_to_ctxt(unsigned n) |
| { |
| if (n == qmi_device0.misc.minor) |
| return &qmi_device0; |
| if (n == qmi_device1.misc.minor) |
| return &qmi_device1; |
| if (n == qmi_device2.misc.minor) |
| return &qmi_device2; |
| return 0; |
| } |
| |
| static int __init qmi_init(void) |
| { |
| int ret; |
| |
| qmi_wq = create_singlethread_workqueue("qmi"); |
| if (qmi_wq == 0) |
| return -ENOMEM; |
| |
| qmi_ctxt_init(&qmi_device0, 0); |
| qmi_ctxt_init(&qmi_device1, 1); |
| qmi_ctxt_init(&qmi_device2, 2); |
| |
| ret = misc_register(&qmi_device0.misc); |
| if (ret == 0) |
| ret = misc_register(&qmi_device1.misc); |
| if (ret == 0) |
| ret = misc_register(&qmi_device2.misc); |
| return ret; |
| } |
| |
| module_init(qmi_init); |