/* arch/arm/mach-msm/smd_qmi.c * * QMI Control Driver -- Manages network data connections. * * Copyright (C) 2007 Google, Inc. * Author: Brian Swetland * * 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 #include #include #include #include #include #include #include #include #include #include #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 */ #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);