| /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| /* |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * 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. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| * 02110-1301, USA. |
| * |
| * Copyright (C) 2011 - 2012 Red Hat, Inc. |
| * Copyright (C) 2010 - 2012 Google, Inc. |
| */ |
| |
| #include <glib.h> |
| #include <string.h> |
| #include <errno.h> |
| #include <sys/ioctl.h> |
| #include <fcntl.h> |
| #include <termios.h> |
| #include <unistd.h> |
| #include <stdlib.h> |
| #include <sys/types.h> |
| |
| typedef u_int16_t u16; |
| typedef u_int8_t u8; |
| typedef u_int32_t u32; |
| typedef u_int64_t u64; |
| |
| typedef unsigned int qbool; |
| #ifndef TRUE |
| #define TRUE 1 |
| #endif |
| #ifndef FALSE |
| #define FALSE 0 |
| #endif |
| |
| /***********************************************************/ |
| |
| static void |
| print_buf (const char *detail, const char *buf, size_t len) |
| { |
| int i = 0, z; |
| qbool newline = FALSE, indent = FALSE; |
| char *f; |
| guint flen; |
| |
| f = g_strdup_printf ("%s (%zu) ", detail, len); |
| flen = strlen (f); |
| g_print ("%s", f); |
| for (i = 0; i < len; i++) { |
| if (indent) { |
| z = flen; |
| while (z--) |
| g_print (" "); |
| indent = FALSE; |
| } |
| g_print ("%02x ", buf[i] & 0xFF); |
| if (((i + 1) % 16) == 0) { |
| g_print ("\n"); |
| newline = TRUE; |
| indent = TRUE; |
| } else |
| newline = FALSE; |
| } |
| |
| if (!newline) |
| g_print ("\n"); |
| } |
| |
| /**************************************************************/ |
| |
| typedef enum { |
| QMI_SVC_CTL = 0x00, |
| QMI_SVC_WDS = 0x01, |
| QMI_SVC_DMS = 0x02, |
| QMI_SVC_NAS = 0x03, |
| QMI_SVC_QOS = 0x04, |
| QMI_SVC_WMS = 0x05, |
| QMI_SVC_PDS = 0x06, |
| QMI_SVC_AUTH = 0x07, |
| QMI_SVC_AT = 0x08, |
| QMI_SVC_VOICE = 0x09, |
| QMI_SVC_CAT2 = 0x0A, |
| QMI_SVC_UIM = 0x0B, |
| QMI_SVC_PBM = 0x0C, |
| QMI_SVC_LOC = 0x10, |
| QMI_SVC_SAR = 0x11, |
| QMI_SVC_RMTFS = 0x14, |
| QMI_SVC_CAT = 0xE0, |
| QMI_SVC_RMS = 0xE1, |
| QMI_SVC_OMA = 0xE2 |
| } qmi_service_type; |
| |
| struct qmux { |
| u8 tf; /* always 1 */ |
| u16 len; |
| u8 ctrl; |
| u8 service; |
| u8 qmicid; |
| } __attribute__((__packed__)); |
| |
| struct qmi_tlv_status { |
| u16 status; |
| u16 error; |
| } __attribute__((__packed__)); |
| |
| struct getcid_req { |
| struct qmux header; |
| u8 req; |
| u8 tid; |
| u16 msgid; |
| u16 tlvsize; |
| u8 service; |
| u16 size; |
| u8 qmisvc; |
| } __attribute__((__packed__)); |
| |
| struct releasecid_req { |
| struct qmux header; |
| u8 req; |
| u8 tid; |
| u16 msgid; |
| u16 tlvsize; |
| u8 rlscid; |
| u16 size; |
| u16 cid; |
| } __attribute__((__packed__)); |
| |
| struct version_info_req { |
| struct qmux header; |
| u8 req; |
| u8 tid; |
| u16 msgid; |
| u16 tlvsize; |
| } __attribute__((__packed__)); |
| |
| struct qmi_ctl_version_info_list_service { |
| u8 service_type; /* QMI_SVC_xxx */ |
| u16 major_version; |
| u16 minor_version; |
| } __attribute__((__packed__)); |
| |
| struct qmi_tlv_ctl_version_info_list { |
| u8 count; |
| struct qmi_ctl_version_info_list_service services[0]; |
| }__attribute__((__packed__)); |
| |
| struct seteventreport_req { |
| struct qmux header; |
| u8 req; |
| u16 tid; |
| u16 msgid; |
| u16 tlvsize; |
| u8 reportchanrate; |
| u16 size; |
| u8 period; |
| u32 mask; |
| } __attribute__((__packed__)); |
| |
| struct getpkgsrvcstatus_req { |
| struct qmux header; |
| u8 req; |
| u16 tid; |
| u16 msgid; |
| u16 tlvsize; |
| } __attribute__((__packed__)); |
| |
| struct getmeid_req { |
| struct qmux header; |
| u8 req; |
| u16 tid; |
| u16 msgid; |
| u16 tlvsize; |
| } __attribute__((__packed__)); |
| |
| struct qmiwds_stats { |
| u32 txok; |
| u32 rxok; |
| u32 txerr; |
| u32 rxerr; |
| u32 txofl; |
| u32 rxofl; |
| u64 txbytesok; |
| u64 rxbytesok; |
| qbool linkstate; |
| qbool reconfigure; |
| }; |
| |
| struct nas_signal_req { |
| struct qmux header; |
| u8 req; |
| u16 tid; |
| u16 msgid; |
| u16 tlvsize; |
| } __attribute__((__packed__)); |
| |
| const size_t qmux_size = sizeof(struct qmux); |
| |
| static void |
| qmux_fill(struct qmux *qmux, u8 service, u16 cid, u16 size) |
| { |
| qmux->tf = 1; |
| qmux->len = size - 1; |
| qmux->ctrl = 0; |
| qmux->service = cid & 0xff; |
| qmux->qmicid = cid >> 8; |
| } |
| |
| static void * |
| qmictl_new_getcid(u8 tid, u8 svctype, size_t *size) |
| { |
| struct getcid_req *req; |
| |
| req = g_malloc0 (sizeof (*req)); |
| req->req = 0x00; |
| req->tid = tid; |
| req->msgid = 0x0022; |
| req->tlvsize = 0x0004; |
| req->service = 0x01; |
| req->size = 0x0001; |
| req->qmisvc = svctype; |
| *size = sizeof(*req); |
| qmux_fill (&req->header, QMI_SVC_CTL, 0, *size); |
| return req; |
| } |
| |
| static void * |
| qmictl_new_releasecid(u8 tid, u16 cid, size_t *size) |
| { |
| struct releasecid_req *req; |
| |
| req = g_malloc0 (sizeof (*req)); |
| req->req = 0x00; |
| req->tid = tid; |
| req->msgid = 0x0023; |
| req->tlvsize = 0x05; |
| req->rlscid = 0x01; |
| req->size = 0x0002; |
| req->cid = cid; |
| *size = sizeof(*req); |
| qmux_fill (&req->header, QMI_SVC_CTL, 0, *size); |
| return req; |
| } |
| |
| static void * |
| qmictl_new_version_info(u8 tid, size_t *size) |
| { |
| struct version_info_req *req; |
| |
| req = g_malloc0 (sizeof (*req)); |
| req->req = 0x00; |
| req->tid = tid; |
| req->msgid = 0x21; |
| req->tlvsize = 0; |
| *size = sizeof(*req); |
| qmux_fill (&req->header, QMI_SVC_CTL, 0, *size); |
| return req; |
| } |
| |
| static void * |
| qmiwds_new_seteventreport(u8 tid, size_t *size) |
| { |
| struct seteventreport_req *req; |
| |
| req = g_malloc0 (sizeof (*req)); |
| req->req = 0x00; |
| req->tid = tid; |
| req->msgid = 0x0001; |
| req->tlvsize = 0x0008; |
| req->reportchanrate = 0x11; |
| req->size = 0x0005; |
| req->period = 0x01; |
| req->mask = 0x000000ff; |
| *size = sizeof(*req); |
| return req; |
| } |
| |
| static void * |
| qmiwds_new_getpkgsrvcstatus(u8 tid, size_t *size) |
| { |
| struct getpkgsrvcstatus_req *req; |
| |
| req = g_malloc0 (sizeof (*req)); |
| req->req = 0x00; |
| req->tid = tid; |
| req->msgid = 0x22; |
| req->tlvsize = 0x0000; |
| *size = sizeof(*req); |
| return req; |
| } |
| |
| static void * |
| qmidms_new_getmeid(u16 cid, u8 tid, size_t *size) |
| { |
| struct getmeid_req *req; |
| |
| req = g_malloc0 (sizeof (*req)); |
| req->req = 0x00; |
| req->tid = tid; |
| req->msgid = 0x25; |
| req->tlvsize = 0x0000; |
| *size = sizeof(*req); |
| qmux_fill (&req->header, QMI_SVC_WDS, cid, *size); |
| return req; |
| } |
| |
| static int |
| qmux_parse(u16 *cid, void *buf, size_t size) |
| { |
| struct qmux *qmux = buf; |
| |
| if (!buf || size < 12) |
| return -ENOMEM; |
| |
| if (qmux->tf != 1 || qmux->len != size - 1 || qmux->ctrl != 0x80) |
| return -EINVAL; |
| |
| *cid = (qmux->qmicid << 8) + qmux->service; |
| return sizeof(*qmux); |
| } |
| |
| static u16 |
| tlv_get(void *msg, u16 msgsize, u8 type, void *buf, u16 bufsize) |
| { |
| u16 pos; |
| u16 msize = 0; |
| |
| if (!msg || !buf) |
| return -ENOMEM; |
| |
| for (pos = 4; pos + 3 < msgsize; pos += msize + 3) { |
| msize = *(u16 *)(msg + pos + 1); |
| if (*(u8 *)(msg + pos) == type) { |
| if (bufsize < msize) |
| return -ENOMEM; |
| |
| memcpy(buf, msg + pos + 3, msize); |
| return msize; |
| } |
| } |
| |
| return -ENOMSG; |
| } |
| |
| static int |
| qmi_msgisvalid(void *msg, u16 size) |
| { |
| struct qmi_tlv_status status; |
| |
| if (tlv_get(msg, size, 2, &status, sizeof (status)) == sizeof (status)) { |
| if (le16toh (status.status != 0)) |
| return le16toh (status.error); |
| else |
| return 0; |
| } |
| return -ENOMSG; |
| } |
| |
| static int |
| qmi_msgid(void *msg, u16 size) |
| { |
| return size < 2 ? -ENODATA : *(u16 *)msg; |
| } |
| |
| static int |
| qmictl_version_info_resp(void *buf, u16 size) |
| { |
| int result, i; |
| u8 offset = sizeof(struct qmux) + 2; |
| u8 svcbuf[100]; |
| struct qmi_tlv_ctl_version_info_list *service_list; |
| struct qmi_ctl_version_info_list_service *svc; |
| |
| if (!buf || size < offset) |
| return -ENOMEM; |
| |
| buf = buf + offset; |
| size -= offset; |
| |
| result = qmi_msgid(buf, size); |
| if (result != 0x21) |
| return -EFAULT; |
| |
| result = qmi_msgisvalid(buf, size); |
| if (result != 0) |
| return -EFAULT; |
| |
| /* Get the services TLV */ |
| result = tlv_get(buf, size, 0x01, svcbuf, sizeof (svcbuf)); |
| if (result < 0) |
| return -EFAULT; |
| |
| service_list = (struct qmi_tlv_ctl_version_info_list *) svcbuf; |
| if (result < (service_list->count * sizeof (struct qmi_ctl_version_info_list_service))) |
| return -EFAULT; |
| |
| svc = &(service_list->services[0]); |
| for (i = 0; i < service_list->count; i++, svc++) { |
| g_message ("SVC: %d v%d.%d", |
| svc->service_type, |
| le16toh (svc->major_version), |
| le16toh (svc->minor_version)); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| qmictl_getcid_resp(void *buf, u16 size, u16 *cid) |
| { |
| int result; |
| u8 offset = sizeof(struct qmux) + 2; |
| |
| if (!buf || size < offset) |
| return -ENOMEM; |
| |
| buf = buf + offset; |
| size -= offset; |
| |
| result = qmi_msgid(buf, size); |
| if (result != 0x22) |
| return -EFAULT; |
| |
| result = qmi_msgisvalid(buf, size); |
| if (result != 0) |
| return -EFAULT; |
| |
| result = tlv_get(buf, size, 0x01, cid, 2); |
| if (result != 2) |
| return -EFAULT; |
| |
| return 0; |
| } |
| |
| static int |
| qmictl_releasecid_resp(void *buf, u16 size) |
| { |
| int result; |
| u8 offset = sizeof(struct qmux) + 2; |
| |
| if (!buf || size < offset) |
| return -ENOMEM; |
| |
| buf = buf + offset; |
| size -= offset; |
| |
| result = qmi_msgid(buf, size); |
| if (result != 0x23) |
| return -EFAULT; |
| |
| result = qmi_msgisvalid(buf, size); |
| if (result != 0) |
| return -EFAULT; |
| |
| return 0; |
| } |
| |
| static int |
| qmiwds_event_resp(void *buf, u16 size, struct qmiwds_stats *stats) |
| { |
| int result; |
| u8 status[2]; |
| |
| u8 offset = sizeof(struct qmux) + 3; |
| |
| if (!buf || size < offset || !stats) |
| return -ENOMEM; |
| |
| buf = buf + offset; |
| size -= offset; |
| |
| result = qmi_msgid(buf, size); |
| if (result == 0x01) { |
| tlv_get(buf, size, 0x10, &stats->txok, 4); |
| tlv_get(buf, size, 0x11, &stats->rxok, 4); |
| tlv_get(buf, size, 0x12, &stats->txerr, 4); |
| tlv_get(buf, size, 0x13, &stats->rxerr, 4); |
| tlv_get(buf, size, 0x14, &stats->txofl, 4); |
| tlv_get(buf, size, 0x15, &stats->rxofl, 4); |
| tlv_get(buf, size, 0x19, &stats->txbytesok, 8); |
| tlv_get(buf, size, 0x1A, &stats->rxbytesok, 8); |
| } else if (result == 0x22) { |
| result = tlv_get(buf, size, 0x01, &status[0], 2); |
| if (result >= 1) |
| stats->linkstate = status[0] == 0x02; |
| if (result == 2) |
| stats->reconfigure = status[1] == 0x01; |
| |
| if (result < 0) |
| return result; |
| } else { |
| return -EFAULT; |
| } |
| |
| return 0; |
| } |
| |
| static int |
| qmidms_meid_resp(void *buf, u16 size, char *meid, int meidsize) |
| { |
| int result; |
| |
| u8 offset = sizeof(struct qmux) + 3; |
| |
| if (!buf || size < offset || meidsize < 14) |
| return -ENOMEM; |
| |
| buf = buf + offset; |
| size -= offset; |
| |
| result = qmi_msgid(buf, size); |
| if (result != 0x25) |
| return -EFAULT; |
| |
| result = qmi_msgisvalid(buf, size); |
| if (result) |
| return -EFAULT; |
| |
| result = tlv_get(buf, size, 0x12, meid, 14); |
| if (result != 14) |
| return -EFAULT; |
| |
| return 0; |
| } |
| |
| static void * |
| qminas_new_signal(u16 cid, u8 tid, size_t *size) |
| { |
| struct nas_signal_req *req; |
| |
| req = g_malloc0 (sizeof (*req)); |
| req->req = 0x00; |
| req->tid = tid; |
| req->msgid = 0x20; |
| req->tlvsize = 0x0000; |
| *size = sizeof(*req); |
| qmux_fill (&req->header, QMI_SVC_NAS, cid, *size); |
| return req; |
| } |
| |
| /* NAS/Get Signal Strength TLV 0x01 */ |
| struct qminas_resp_signalstrength { |
| u8 dbm; |
| u8 act; |
| } __attribute__((__packed__)); |
| |
| static int |
| qminas_signal_resp(void *buf, u16 size, u8 *dbm, u8 *act) |
| { |
| int result; |
| struct qminas_resp_signalstrength signal; |
| |
| u8 offset = sizeof(struct qmux) + 3; |
| |
| if (!buf || size < offset) |
| return -ENOMEM; |
| |
| buf = buf + offset; |
| size -= offset; |
| |
| result = qmi_msgid(buf, size); |
| if (result != 0x20) |
| return -EFAULT; |
| |
| result = qmi_msgisvalid(buf, size); |
| if (result) |
| return -EFAULT; |
| |
| result = tlv_get(buf, size, 0x01, &signal, sizeof (signal)); |
| if (result != sizeof (signal)) |
| return -EFAULT; |
| |
| *dbm = signal.dbm; |
| *act = signal.act; |
| return 0; |
| } |
| |
| /*****************************************************/ |
| |
| static size_t |
| send_and_wait_reply (int fd, void *b, size_t blen, char *reply, size_t rlen) |
| { |
| ssize_t num; |
| fd_set in; |
| int result; |
| struct timeval timeout = { 1, 0 }; |
| |
| print_buf (">>>", b, blen); |
| num = write (fd, b, blen); |
| if (num != blen) { |
| g_warning ("Failed to write: wrote %zd err %d", num, errno); |
| return 0; |
| } |
| |
| FD_ZERO (&in); |
| FD_SET (fd, &in); |
| result = select (fd + 1, &in, NULL, NULL, &timeout); |
| if (result != 1 || !FD_ISSET (fd, &in)) { |
| g_warning ("No data pending"); |
| return 0; |
| } |
| |
| errno = 0; |
| num = read (fd, reply, rlen); |
| if (num < 0) { |
| g_warning ("read error %d", errno); |
| return 0; |
| } |
| print_buf ("<<<", reply, num); |
| return num; |
| } |
| |
| /* CTL service transaction ID */ |
| static u8 ctl_tid = 1; |
| |
| static int |
| get_meid (int fd) |
| { |
| void *b; |
| size_t blen; |
| u8 dms_tid = 1; |
| u16 dms_cid = 0; |
| char reply[2048]; |
| size_t rlen; |
| char meid[16]; |
| int err; |
| |
| /* Allocate a DMS client ID */ |
| b = qmictl_new_getcid (ctl_tid++, QMI_SVC_DMS, &blen); |
| g_assert (b); |
| |
| rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply)); |
| g_free (b); |
| if (rlen <= 0) |
| return 1; |
| |
| err = qmictl_getcid_resp (reply, rlen, &dms_cid); |
| if (err < 0) { |
| g_warning ("Failed to get DMS client ID: %d", err); |
| return 1; |
| } |
| |
| g_message ("DMS CID %d 0x%X", dms_cid, dms_cid); |
| |
| /* Get the MEID */ |
| b = qmidms_new_getmeid(dms_cid, dms_tid++, &blen); |
| g_assert (b); |
| |
| rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply)); |
| g_free (b); |
| if (rlen <= 0) |
| return 1; |
| |
| memset (meid, 0, sizeof (meid)); |
| err = qmidms_meid_resp (reply, rlen, meid, sizeof (meid) - 1); |
| if (err < 0) |
| g_warning ("Failed to get MEID: %d", err); |
| else |
| g_message ("MEID: %s", meid); |
| |
| /* Relese the DMS client ID */ |
| b = qmictl_new_releasecid (ctl_tid++, dms_cid, &blen); |
| g_assert (b); |
| |
| rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply)); |
| g_free (b); |
| if (rlen <= 0) |
| return 1; |
| |
| err = qmictl_releasecid_resp (reply, rlen); |
| if (err < 0) { |
| g_warning ("Failed to release DMS client ID: %d", err); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static const char * |
| act_to_string (u8 act) |
| { |
| switch (act) { |
| case 0: |
| return "no service"; |
| case 1: |
| return "CDMA2000 1x"; |
| case 2: |
| return "CDMA2000 HRPD/EVDO"; |
| case 3: |
| return "AMPS"; |
| case 4: |
| return "GSM"; |
| case 5: |
| return "UMTS"; |
| case 8: |
| return "LTE"; |
| default: |
| break; |
| } |
| return "unknown"; |
| } |
| |
| static int |
| get_signal (int fd) |
| { |
| void *b; |
| size_t blen; |
| u8 nas_tid = 1; |
| u16 nas_cid = 0; |
| char reply[2048]; |
| size_t rlen; |
| int err; |
| u8 dbm = 0, act = 0; |
| |
| /* Allocate a NAS client ID */ |
| b = qmictl_new_getcid (ctl_tid++, QMI_SVC_NAS, &blen); |
| g_assert (b); |
| |
| rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply)); |
| g_free (b); |
| if (rlen <= 0) |
| return 1; |
| |
| err = qmictl_getcid_resp (reply, rlen, &nas_cid); |
| if (err < 0) { |
| g_warning ("Failed to get NAS client ID: %d", err); |
| return 1; |
| } |
| |
| g_message ("NAS CID %d 0x%X", nas_cid, nas_cid); |
| |
| /* Get the signal strength */ |
| b = qminas_new_signal(nas_cid, nas_tid++, &blen); |
| g_assert (b); |
| |
| rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply)); |
| g_free (b); |
| if (rlen <= 0) |
| return 1; |
| |
| err = qminas_signal_resp (reply, rlen, &dbm, &act); |
| if (err < 0) |
| g_warning ("Failed to get signal: %d", err); |
| else { |
| g_message ("dBm: -%d", 0x100 - (dbm & 0xFF)); |
| g_message ("AcT: %s", act_to_string (act)); |
| } |
| |
| /* Relese the NAS client ID */ |
| b = qmictl_new_releasecid (ctl_tid++, nas_cid, &blen); |
| g_assert (b); |
| |
| rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply)); |
| g_free (b); |
| if (rlen <= 0) |
| return 1; |
| |
| err = qmictl_releasecid_resp (reply, rlen); |
| if (err < 0) { |
| g_warning ("Failed to release NAS client ID: %d", err); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| int main(int argc, char *argv[]) |
| { |
| int fd; |
| void *b; |
| size_t blen; |
| u8 ctl_tid = 1; |
| char reply[2048]; |
| size_t rlen; |
| int err; |
| |
| if (argc != 2) { |
| g_warning ("usage: %s <port>", argv[0]); |
| return 1; |
| } |
| |
| errno = 0; |
| fd = open (argv[1], O_RDWR | O_EXCL | O_NONBLOCK | O_NOCTTY); |
| if (fd < 0) { |
| g_warning ("%s: open failed: %d", argv[1], errno); |
| return 1; |
| } |
| |
| /* Send the ready request */ |
| b = qmictl_new_version_info (ctl_tid++, &blen); |
| g_assert (b); |
| |
| rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply)); |
| g_free (b); |
| if (rlen <= 0) |
| goto out; |
| |
| err = qmictl_version_info_resp (reply, rlen); |
| if (err < 0) { |
| g_warning ("Failed to get version info: %d", err); |
| goto out; |
| } |
| |
| get_meid (fd); |
| get_signal (fd); |
| |
| out: |
| close (fd); |
| return 0; |
| } |
| |