blob: c1c8e754921127bd275d40e3602428e8d34c9956 [file] [log] [blame]
/*
* Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "qmidev.h"
#include <assert.h>
#include <errno.h>
#include <glib.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <glib.h>
#include <libqmi.h>
#include "dev.h"
#include "error.h"
#include "file.h"
#include "mock.h"
#include "poller.h"
#include "qmi.h"
#include "qmictl.h"
#include "qmimsg.h"
#include "util.h"
struct qmidev {
void *priv;
struct poller *poller;
struct dev *dev;
struct polled *dev_polled;
int timerfd;
struct polled *timer_polled;
GList *callbacks;
GList *listeners;
GList *services;
int connected;
uint8_t control_tid;
};
struct qmisvc {
uint8_t sid;
uint8_t cid;
uint16_t next_tid;
};
struct listener {
struct listen_criteria criteria;
qmidev_listen_callback_fn callback;
void *context;
};
struct callback {
callback_fn func;
void *context;
};
static void call_listeners(struct qmidev *qmidev, struct qmimsg *message);
static void call_callbacks(struct qmidev *qmidev);
static struct qmidev *qmidev_new(void)
{
struct qmidev *qmidev = g_slice_new(struct qmidev);
qmidev->priv = NULL;
qmidev->poller = poller_new();
qmidev->dev = NULL;
qmidev->dev_polled = NULL;
qmidev->callbacks = NULL;
qmidev->listeners = NULL;
qmidev->connected = 0;
qmidev->control_tid = 1;
return qmidev;
}
struct qmidev *qmidev_new_file(const char *path)
{
assert(path);
struct qmidev *qmidev = qmidev_new();
struct dev *dev = file_new(qmidev, path);
if (!dev) {
free(qmidev);
return NULL;
}
qmidev->dev = dev;
return qmidev;
}
struct qmidev *qmidev_new_mock(void)
{
struct qmidev *qmidev = qmidev_new();
struct dev *dev = mock_new();
qmidev->dev = dev;
return qmidev;
}
void *qmidev_priv(struct qmidev *qmidev)
{
assert(qmidev);
return qmidev->priv;
}
void qmidev_set_priv(struct qmidev *qmidev, void *priv)
{
assert(qmidev);
qmidev->priv = priv;
}
int qmidev_fd(struct qmidev *qmidev)
{
assert(qmidev);
return poller_fd(qmidev->poller);
}
int qmidev_process(struct qmidev *qmidev)
{
assert(qmidev);
call_callbacks(qmidev);
return poller_poll(qmidev->poller);
}
static struct qmisvc *qmisvc_alloc(uint8_t sid)
{
struct qmisvc *svc = g_slice_new(struct qmisvc);
svc->sid = sid;
svc->cid = QMI_CID_NONE;
svc->next_tid = 1;
return svc;
}
static void qmisvc_free(struct qmisvc *svc)
{
g_slice_free(struct qmisvc, svc);
}
struct connect_context {
GQueue *pending_services;
struct qmisvc *current_service;
qmidev_response_fn caller_callback;
void *caller_context;
};
static void connect_next_service(struct qmidev *qmidev,
struct connect_context *context);
static void got_cid(struct qmidev *qmidev,
void *data,
int status,
uint8_t cid)
{
struct connect_context *context = data;
if (!status) {
context->current_service->cid = cid;
qmidev->services = g_list_prepend(qmidev->services,
context->current_service);
} else {
qmisvc_free(context->current_service);
}
connect_next_service(qmidev, context);
}
static void connect_next_service(struct qmidev *qmidev,
struct connect_context *context)
{
if (g_queue_is_empty(context->pending_services)) {
context->caller_callback(qmidev, context->caller_context, 0);
g_queue_free(context->pending_services);
g_slice_free(struct connect_context, context);
return;
}
context->current_service = g_queue_pop_head(context->pending_services);
/* TODO: Check return */
qmictl_get_cid(qmidev, context->current_service->sid,
&got_cid, context);
}
int qmidev_connect(struct qmidev *qmidev,
qmidev_response_fn caller_callback,
void *caller_context)
{
assert(qmidev);
assert(caller_callback);
if (qmidev->connected)
return QMI_ERR_ALREADY_CONNECTED;
int result = dev_open(qmidev->dev);
/* TODO: Should this go through the callback instead? */
if (result)
return result;
qmidev->connected = 1;
struct connect_context *context = g_slice_new(struct connect_context);
context->caller_callback = caller_callback;
context->caller_context = caller_context;
context->current_service = NULL;
context->pending_services = g_queue_new();
qmidev->services = g_list_append(qmidev->services, qmisvc_alloc(QMI_SVC_CTL));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_WDS));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_DMS));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_NAS));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_QOS));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_WMS));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_PDS));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_AUTH));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_VOICE));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_CAT));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_RMS));
g_queue_push_tail(context->pending_services, qmisvc_alloc(QMI_SVC_OMA));
connect_next_service(qmidev, context);
return 0;
}
struct disconnect_context {
struct qmisvc *current_service;
qmidev_response_fn caller_callback;
void *caller_context;
};
static void services_disconnected(struct qmidev *qmidev,
struct disconnect_context *context)
{
int result = dev_close(qmidev->dev);
qmidev->connected = 0;
context->caller_callback(qmidev, context->caller_context, result);
g_slice_free(struct disconnect_context, context);
}
static void disconnect_next_service(struct qmidev *qmidev,
struct disconnect_context *context);
static void released_cid(struct qmidev *qmidev,
void *data,
int status)
{
struct disconnect_context *context = data;
if (status) {
/* TODO: What do we do if release fails? */
}
qmisvc_free(context->current_service);
disconnect_next_service(qmidev, context);
}
static void disconnect_next_service(struct qmidev *qmidev,
struct disconnect_context *context)
{
if (!qmidev->services) {
services_disconnected(qmidev, context);
return;
}
context->current_service = qmidev->services->data;
qmidev->services = g_list_remove(qmidev->services, context->current_service);
/* TODO: Check return */
if (context->current_service->sid != QMI_SVC_CTL) {
qmictl_release_cid(qmidev, context->current_service->sid,
context->current_service->cid,
&released_cid, context);
} else {
qmisvc_free(context->current_service);
disconnect_next_service(qmidev, context);
}
}
int qmidev_disconnect(struct qmidev *qmidev,
qmidev_response_fn caller_callback,
void *caller_context)
{
assert(qmidev);
assert(caller_callback);
if (!qmidev->connected)
return QMI_ERR_NOT_CONNECTED;
struct disconnect_context *context = g_slice_new(struct disconnect_context);
context->caller_callback = caller_callback;
context->caller_context = caller_context;
context->current_service = NULL;
disconnect_next_service(qmidev, context);
return 0;
}
gint service_compare_sid(gconstpointer a, gconstpointer b)
{
const struct qmisvc *svc_a = a;
const struct qmisvc *svc_b = b;
return svc_a->sid - svc_b->sid;
}
struct qmisvc *find_service(struct qmidev *qmidev, uint8_t service_id)
{
struct qmisvc wanted;
wanted.sid = service_id;
GList *found_list = g_list_find_custom(qmidev->services, &wanted,
&service_compare_sid);
if (found_list)
return (struct qmisvc *) found_list->data;
else
return NULL;
}
uint16_t get_free_tid(struct qmisvc *service)
{
uint16_t next_tid = service->next_tid;
/* CTL uses 8-bit transaction IDs; other services use 16-bit. */
uint16_t max_tid = (service->sid == QMI_SVC_CTL) ? 0x00FF : 0xFFFF;
if (service->next_tid < max_tid)
service->next_tid++;
else
service->next_tid = 0x0001;
return next_tid;
}
int qmidev_make_request(struct qmidev *qmidev,
uint8_t service_id,
uint16_t req_msg_id,
struct qmimsg **message_out)
{
struct qmisvc *service;
uint16_t transaction_id;
struct qmimsg *message;
int result;
assert(qmidev);
assert(message_out);
service = find_service(qmidev, service_id);
if (!service)
return QMI_ERR_SERVICE_UNAVAILABLE;
transaction_id = get_free_tid(service);
result = qmimsg_new(0, service_id, service->cid,
0, transaction_id, req_msg_id,
&message);
if (result)
return result;
*message_out = message;
return 0;
}
struct request_context {
uint16_t resp_msg_id;
qmidev_request_fn caller_callback;
void *caller_context;
};
int got_reply(struct qmidev *qmidev, void *data, struct qmimsg *message)
{
struct request_context *context = data;
int result;
/* TODO: Check response message ID. */
result = qmi_result(message);
if (result)
goto out;
out:
context->caller_callback(qmidev, context->caller_context, result, message);
g_slice_free(struct request_context, context);
return 1;
}
int qmidev_send_request(struct qmidev *qmidev,
struct qmimsg *req_msg,
uint16_t resp_msg_id,
qmidev_request_fn caller_callback,
void *caller_context)
{
assert(qmidev);
assert(req_msg);
assert(caller_callback);
if (!qmidev->connected)
return QMI_ERR_NOT_CONNECTED;
struct request_context *context = g_slice_new(struct request_context);
context->caller_callback = caller_callback;
context->caller_context = caller_context;
context->resp_msg_id = resp_msg_id;
struct listen_criteria criteria;
qmimsg_get_header(req_msg,
&criteria.service,
&criteria.client,
&criteria.transaction);
struct listener *listener = qmidev_listen(qmidev, &criteria,
&got_reply, context);
/* TODO: Do something with listener? Currently we don't need it for
anything, but once we add timeouts we'll need to be able to cancel
it. */
int result = qmidev_send(qmidev, req_msg);
if (result) {
qmidev_cancel_listen(qmidev, listener);
g_slice_free(struct request_context, context);
qmimsg_free(req_msg);
return result;
}
return 0;
}
static int dev_read_fn(void *ctx, void *buf, size_t len)
{
assert(ctx);
struct qmidev *qmidev = ctx;
assert(qmidev->dev);
return dev_read(qmidev->dev, buf, len);
}
static void dev_read_cb(struct polled *polled)
{
assert(polled);
struct qmidev *qmidev = polled_priv(polled);
assert(qmidev);
struct qmimsg *msg;
int result = qmimsg_read(&dev_read_fn, qmidev, &msg);
/* TODO: Do something. */
if (result)
return;
call_listeners(qmidev, msg);
qmimsg_free(msg);
}
static void dev_close_cb(struct polled *polled)
{
assert(polled);
struct qmidev *qmidev = polled_priv(polled);
assert(qmidev);
qmidev_clear_dev_fd(qmidev);
}
int qmidev_set_dev_fd(struct qmidev *qmidev, int fd)
{
assert(qmidev);
assert(fd >= 0);
assert(!qmidev->dev_polled);
qmidev->dev_polled = poller_add(qmidev->poller, fd);
if (!qmidev->dev_polled)
return QMI_ERR_INTERNAL;
polled_set_priv(qmidev->dev_polled, qmidev);
polled_set_read(qmidev->dev_polled, &dev_read_cb);
polled_set_close(qmidev->dev_polled, &dev_close_cb);
polled_update(qmidev->dev_polled);
return 0;
}
int qmidev_clear_dev_fd(struct qmidev *qmidev)
{
assert(qmidev);
assert(qmidev->dev_polled);
/* Make sure we don't get called again. */
polled_set_close(qmidev->dev_polled, NULL);
polled_update(qmidev->dev_polled);
/* will be freed by the poller when the fd closes. */
qmidev->dev_polled = NULL;
return 0;
}
static int dev_write_fn(void *ctx, const void *buf, size_t len)
{
assert(ctx);
struct qmidev *qmidev = ctx;
assert(qmidev->dev);
return dev_write(qmidev->dev, buf, len);
}
int qmidev_send(struct qmidev *qmidev, struct qmimsg *msg)
{
assert(qmidev);
assert(qmidev->connected);
return qmimsg_write(msg, &dev_write_fn, qmidev);
}
struct polled *qmidev_poll(struct qmidev *qmidev, int fd)
{
assert(qmidev);
return poller_add(qmidev->poller, fd);
}
void qmidev_call_later(struct qmidev *qmidev, callback_fn func, void *context)
{
assert(qmidev);
assert(func);
struct callback *callback = g_slice_new(struct callback);
callback->func = func;
callback->context = context;
qmidev->callbacks = g_list_append(qmidev->callbacks, callback);
}
static void call_callback(void *data, void *user_data)
{
struct qmidev *qmidev = user_data;
struct callback *callback = data;
callback->func(qmidev, callback->context);
g_slice_free(struct callback, callback);
}
static void call_callbacks(struct qmidev *qmidev)
{
g_list_foreach(qmidev->callbacks, call_callback, qmidev);
g_list_free(qmidev->callbacks);
qmidev->callbacks = NULL;
}
struct listener *qmidev_listen(struct qmidev *qmidev,
struct listen_criteria *criteria,
qmidev_listen_callback_fn callback,
void *context)
{
assert(qmidev);
assert(criteria);
assert(callback);
struct listener *listener = g_slice_new(struct listener);
listener->criteria = *criteria;
listener->callback = callback;
listener->context = context;
qmidev->listeners = g_list_prepend(qmidev->listeners, listener);
return listener;
}
void qmidev_cancel_listen(struct qmidev *qmidev, struct listener *listener)
{
assert(qmidev);
assert(listener);
qmidev->listeners = g_list_remove(qmidev->listeners, listener);
}
struct call_listener_context {
struct qmidev *qmidev;
struct qmimsg *message;
uint8_t service;
uint8_t client;
uint16_t transaction;
};
static void call_listener(void *data, void *user_data)
{
struct call_listener_context *context = user_data;
struct listener *listener = data;
if (listener->criteria.service != context->service)
return;
if ((listener->criteria.client != QMI_CID_BROADCAST) &&
(listener->criteria.client != context->client))
return;
if ((listener->criteria.transaction != 0) &&
(listener->criteria.transaction != context->transaction))
return;
int result = listener->callback(context->qmidev,
listener->context,
context->message);
if (result) {
/* The glib documentation does not note this, but it is in safe to call
g_list_remove while called back from g_list_foreach. */
qmidev_cancel_listen(context->qmidev, listener);
}
}
static void call_listeners(struct qmidev *qmidev, struct qmimsg *message)
{
struct call_listener_context context;
context.qmidev = qmidev;
context.message = message;
qmimsg_get_header(message,
&context.service, &context.client, &context.transaction);
g_list_foreach(qmidev->listeners, &call_listener, &context);
}