summaryrefslogtreecommitdiff
path: root/gatchat/gatchat.c
diff options
context:
space:
mode:
Diffstat (limited to 'gatchat/gatchat.c')
-rw-r--r--gatchat/gatchat.c1104
1 files changed, 1104 insertions, 0 deletions
diff --git a/gatchat/gatchat.c b/gatchat/gatchat.c
new file mode 100644
index 00000000..e54e5d93
--- /dev/null
+++ b/gatchat/gatchat.c
@@ -0,0 +1,1104 @@
+/*
+ *
+ * AT chat library with GLib integration
+ *
+ * Copyright (C) 2008-2009 Intel Corporation. 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.
+ *
+ * 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+
+#include <glib.h>
+
+#include "ringbuffer.h"
+#include "gatresult.h"
+#include "gatchat.h"
+
+/* #define WRITE_SCHEDULER_DEBUG 1 */
+
+static void g_at_chat_wakeup_writer(GAtChat *chat);
+
+enum chat_state {
+ PARSER_STATE_IDLE = 0,
+ PARSER_STATE_INITIAL_CR,
+ PARSER_STATE_INITIAL_LF,
+ PARSER_STATE_RESPONSE,
+ PARSER_STATE_TERMINATOR_CR,
+ PARSER_STATE_RESPONSE_COMPLETE,
+ PARSER_STATE_PDU,
+ PARSER_STATE_PDU_CR,
+ PARSER_STATE_PDU_COMPLETE,
+ PARSER_STATE_PROMPT,
+ PARSER_STATE_PROMPT_COMPLETE
+};
+
+struct at_command {
+ char *cmd;
+ char **prefixes;
+ guint id;
+ GAtResultFunc callback;
+ gpointer user_data;
+ GDestroyNotify notify;
+};
+
+struct at_notify_node {
+ guint id;
+ GAtNotifyFunc callback;
+ gpointer user_data;
+ GDestroyNotify notify;
+};
+
+struct at_notify {
+ GSList *nodes;
+ gboolean pdu;
+};
+
+struct _GAtChat {
+ gint ref_count; /* Ref count */
+ guint next_cmd_id; /* Next command id */
+ guint next_notify_id; /* Next notify id */
+ guint read_watch; /* GSource read id, 0 if none */
+ guint write_watch; /* GSource write id, 0 if none */
+ GIOChannel *channel; /* channel */
+ GQueue *command_queue; /* Command queue */
+ guint cmd_bytes_written; /* bytes written from cmd */
+ GHashTable *notify_list; /* List of notification reg */
+ GAtDisconnectFunc user_disconnect; /* user disconnect func */
+ gpointer user_disconnect_data; /* user disconnect data */
+ struct ring_buffer *buf; /* Current read buffer */
+ guint read_so_far; /* Number of bytes processed */
+ gboolean disconnecting; /* Whether we're disconnecting */
+ enum chat_state state; /* Current chat state */
+ int flags;
+ char *pdu_notify; /* Unsolicited Resp w/ PDU */
+ GSList *response_lines; /* char * lines of the response */
+ char *wakeup; /* command sent to wakeup modem */
+ gdouble inactivity_time; /* Period of inactivity */
+ guint wakeup_timeout; /* How long to wait for resp */
+ GTimer *wakeup_timer; /* Keep track of elapsed time */
+};
+
+static gint at_notify_node_compare_by_id(gconstpointer a, gconstpointer b)
+{
+ const struct at_notify_node *node = a;
+ guint id = GPOINTER_TO_UINT(b);
+
+ if (node->id < id)
+ return -1;
+
+ if (node->id > id)
+ return 1;
+
+ return 0;
+}
+
+static void at_notify_node_destroy(struct at_notify_node *node)
+{
+ if (node->notify)
+ node->notify(node->user_data);
+
+ g_free(node);
+}
+
+static void at_notify_destroy(struct at_notify *notify)
+{
+ g_slist_foreach(notify->nodes, (GFunc) at_notify_node_destroy, NULL);
+ g_free(notify);
+}
+
+static gint at_command_compare_by_id(gconstpointer a, gconstpointer b)
+{
+ const struct at_command *command = a;
+ guint id = GPOINTER_TO_UINT(b);
+
+ if (command->id < id)
+ return -1;
+
+ if (command->id > id)
+ return 1;
+
+ return 0;
+}
+
+static struct at_command *at_command_create(const char *cmd,
+ const char **prefix_list,
+ GAtResultFunc func,
+ gpointer user_data,
+ GDestroyNotify notify)
+{
+ struct at_command *c;
+ gsize len;
+ char **prefixes = NULL;
+
+ if (prefix_list) {
+ int num_prefixes = 0;
+ int i;
+
+ while (prefix_list[num_prefixes])
+ num_prefixes += 1;
+
+ prefixes = g_new(char *, num_prefixes + 1);
+
+ for (i = 0; i < num_prefixes; i++)
+ prefixes[i] = strdup(prefix_list[i]);
+
+ prefixes[num_prefixes] = NULL;
+ }
+
+ c = g_try_new0(struct at_command, 1);
+
+ if (!c)
+ return 0;
+
+ len = strlen(cmd);
+ c->cmd = g_try_new(char, len + 2);
+
+ if (!c->cmd) {
+ g_free(c);
+ return 0;
+ }
+
+ memcpy(c->cmd, cmd, len);
+
+ /* If we have embedded '\r' then this is a command expecting a prompt
+ * from the modem. Embed Ctrl-Z at the very end automatically
+ */
+ if (strchr(cmd, '\r'))
+ c->cmd[len] = 26;
+ else
+ c->cmd[len] = '\r';
+
+ c->cmd[len+1] = '\0';
+
+ c->prefixes = prefixes;
+ c->callback = func;
+ c->user_data = user_data;
+ c->notify = notify;
+
+ return c;
+}
+
+static void at_command_destroy(struct at_command *cmd)
+{
+ if (cmd->notify)
+ cmd->notify(cmd->user_data);
+
+ g_strfreev(cmd->prefixes);
+ g_free(cmd->cmd);
+ g_free(cmd);
+}
+
+static void g_at_chat_cleanup(GAtChat *chat)
+{
+ struct at_command *c;
+
+ ring_buffer_free(chat->buf);
+ chat->buf = NULL;
+
+ /* Cleanup pending commands */
+ while ((c = g_queue_pop_head(chat->command_queue)))
+ at_command_destroy(c);
+
+ g_queue_free(chat->command_queue);
+ chat->command_queue = NULL;
+
+ /* Cleanup any response lines we have pending */
+ g_slist_foreach(chat->response_lines, (GFunc)g_free, NULL);
+ g_slist_free(chat->response_lines);
+ chat->response_lines = NULL;
+
+ /* Cleanup registered notifications */
+ g_hash_table_destroy(chat->notify_list);
+ chat->notify_list = NULL;
+
+ if (chat->pdu_notify) {
+ g_free(chat->pdu_notify);
+ chat->pdu_notify = NULL;
+ }
+
+ if (chat->wakeup) {
+ g_free(chat->wakeup);
+ chat->wakeup = NULL;
+ }
+
+ if (chat->wakeup_timer) {
+ g_timer_destroy(chat->wakeup_timer);
+ chat->wakeup_timer = 0;
+ }
+}
+
+static void read_watcher_destroy_notify(GAtChat *chat)
+{
+ chat->read_watch = 0;
+
+ if (chat->disconnecting)
+ return;
+
+ chat->channel = NULL;
+
+ g_at_chat_cleanup(chat);
+
+ if (chat->user_disconnect)
+ chat->user_disconnect(chat->user_disconnect_data);
+}
+
+static void write_watcher_destroy_notify(GAtChat *chat)
+{
+ chat->write_watch = 0;
+}
+
+static void at_notify_call_callback(gpointer data, gpointer user_data)
+{
+ struct at_notify_node *node = data;
+ GAtResult *result = user_data;
+
+ node->callback(result, node->user_data);
+}
+
+static gboolean g_at_chat_match_notify(GAtChat *chat, char *line)
+{
+ GHashTableIter iter;
+ struct at_notify *notify;
+ char *prefix;
+ gpointer key, value;
+ gboolean ret = FALSE;
+ GAtResult result;
+
+ g_hash_table_iter_init(&iter, chat->notify_list);
+ result.lines = 0;
+ result.final_or_pdu = 0;
+
+ while (g_hash_table_iter_next(&iter, &key, &value)) {
+ prefix = key;
+ notify = value;
+
+ if (!g_str_has_prefix(line, key))
+ continue;
+
+ if (notify->pdu) {
+ chat->pdu_notify = line;
+ chat->state = PARSER_STATE_PDU;
+ return TRUE;
+ }
+
+ if (!result.lines)
+ result.lines = g_slist_prepend(NULL, line);
+
+ g_slist_foreach(notify->nodes, at_notify_call_callback,
+ &result);
+ ret = TRUE;
+ }
+
+ if (ret) {
+ g_slist_free(result.lines);
+ g_free(line);
+ chat->state = PARSER_STATE_IDLE;
+ }
+
+ return ret;
+}
+
+static void g_at_chat_finish_command(GAtChat *p, gboolean ok,
+ char *final)
+{
+ struct at_command *cmd = g_queue_pop_head(p->command_queue);
+
+ /* Cannot happen, but lets be paranoid */
+ if (!cmd)
+ return;
+
+ if (cmd->callback) {
+ GAtResult result;
+
+ p->response_lines = g_slist_reverse(p->response_lines);
+
+ result.final_or_pdu = final;
+ result.lines = p->response_lines;
+
+ cmd->callback(ok, &result, cmd->user_data);
+ }
+
+ g_slist_foreach(p->response_lines, (GFunc)g_free, NULL);
+ g_slist_free(p->response_lines);
+ p->response_lines = NULL;
+
+ g_free(final);
+
+ at_command_destroy(cmd);
+
+ p->cmd_bytes_written = 0;
+
+ if (g_queue_peek_head(p->command_queue))
+ g_at_chat_wakeup_writer(p);
+}
+
+struct terminator_info {
+ const char *terminator;
+ int len;
+ gboolean success;
+};
+
+static struct terminator_info terminator_table[] = {
+ { "OK", -1, TRUE },
+ { "ERROR", -1, FALSE },
+ { "NO DIALTONE", -1, FALSE },
+ { "BUSY", -1, FALSE },
+ { "NO CARRIER", -1, FALSE },
+ { "CONNECT", -1, TRUE },
+ { "NO ANSWER", -1, FALSE },
+ { "+CMS ERROR:", 11, FALSE },
+ { "+CME ERROR:", 11, FALSE },
+ { "+EXT ERROR:", 11, FALSE }
+};
+
+static gboolean g_at_chat_handle_command_response(GAtChat *p,
+ struct at_command *cmd,
+ char *line)
+{
+ int i;
+ int size = sizeof(terminator_table) / sizeof(struct terminator_info);
+
+ p->state = PARSER_STATE_IDLE;
+
+ for (i = 0; i < size; i++) {
+ struct terminator_info *info = &terminator_table[i];
+
+ if (info->len == -1 && !strcmp(line, info->terminator)) {
+ g_at_chat_finish_command(p, info->success, line);
+ return TRUE;
+ }
+
+ if (info->len > 0 &&
+ !strncmp(line, info->terminator, info->len)) {
+ g_at_chat_finish_command(p, info->success, line);
+ return TRUE;
+ }
+ }
+
+ if (cmd->prefixes) {
+ int i;
+
+ for (i = 0; cmd->prefixes[i]; i++)
+ if (g_str_has_prefix(line, cmd->prefixes[i]))
+ goto out;
+
+ return FALSE;
+ }
+
+out:
+ p->response_lines = g_slist_prepend(p->response_lines,
+ line);
+
+ return TRUE;
+}
+
+static void have_line(GAtChat *p)
+{
+ /* We're not going to copy terminal <CR><LF> */
+ unsigned int len = p->read_so_far - 2;
+ char *str;
+ struct at_command *cmd;
+
+ /* If we have preceding <CR><LF> modify the len */
+ if ((p->flags & G_AT_CHAT_FLAG_NO_LEADING_CRLF) == 0)
+ len -= 2;
+
+ /* Make sure we have terminal null */
+ str = g_try_new(char, len + 1);
+
+ if (!str) {
+ ring_buffer_drain(p->buf, p->read_so_far);
+ return;
+ }
+
+ if ((p->flags & G_AT_CHAT_FLAG_NO_LEADING_CRLF) == 0)
+ ring_buffer_drain(p->buf, 2);
+ ring_buffer_read(p->buf, str, len);
+ ring_buffer_drain(p->buf, 2);
+
+ str[len] = '\0';
+
+ /* Check for echo, this should not happen, but lets be paranoid */
+ if (!strncmp(str, "AT", 2) == TRUE)
+ goto done;
+
+ cmd = g_queue_peek_head(p->command_queue);
+
+ if (cmd && p->cmd_bytes_written == strlen(cmd->cmd) &&
+ g_at_chat_handle_command_response(p, cmd, str))
+ return;
+
+ if (g_at_chat_match_notify(p, str) == TRUE)
+ return;
+
+done:
+ /* No matches & no commands active, ignore line */
+ g_free(str);
+ p->state = PARSER_STATE_IDLE;
+}
+
+static void have_pdu(GAtChat *p)
+{
+ unsigned int len = p->read_so_far - 2;
+ char *pdu;
+ GHashTableIter iter;
+ struct at_notify *notify;
+ char *prefix;
+ gpointer key, value;
+ GAtResult result;
+
+ pdu = g_try_new(char, len + 1);
+
+ if (!pdu) {
+ ring_buffer_drain(p->buf, p->read_so_far);
+ goto out;
+ }
+
+ ring_buffer_read(p->buf, pdu, len);
+ ring_buffer_drain(p->buf, 2);
+
+ pdu[len] = '\0';
+
+ result.lines = g_slist_prepend(NULL, p->pdu_notify);
+ result.final_or_pdu = pdu;
+
+ g_hash_table_iter_init(&iter, p->notify_list);
+
+ while (g_hash_table_iter_next(&iter, &key, &value)) {
+ prefix = key;
+ notify = value;
+
+ if (!g_str_has_prefix(p->pdu_notify, prefix))
+ continue;
+
+ if (!notify->pdu)
+ continue;
+
+ g_slist_foreach(notify->nodes, at_notify_call_callback,
+ &result);
+ }
+
+ g_slist_free(result.lines);
+
+out:
+ g_free(p->pdu_notify);
+ p->pdu_notify = NULL;
+
+ if (pdu)
+ g_free(pdu);
+
+ p->state = PARSER_STATE_IDLE;
+}
+
+static inline void parse_char(GAtChat *chat, char byte)
+{
+ switch (chat->state) {
+ case PARSER_STATE_IDLE:
+ if (byte == '\r')
+ chat->state = PARSER_STATE_INITIAL_CR;
+ else if (chat->flags & G_AT_CHAT_FLAG_NO_LEADING_CRLF) {
+ if (byte == '>')
+ chat->state = PARSER_STATE_PROMPT;
+ else
+ chat->state = PARSER_STATE_RESPONSE;
+ }
+ break;
+
+ case PARSER_STATE_INITIAL_CR:
+ if (byte == '\n')
+ chat->state = PARSER_STATE_INITIAL_LF;
+ else if (byte != '\r' && /* Echo & no <CR><LF>?! */
+ (chat->flags & G_AT_CHAT_FLAG_NO_LEADING_CRLF))
+ chat->state = PARSER_STATE_RESPONSE;
+ else if (byte != '\r')
+ chat->state = PARSER_STATE_IDLE;
+ break;
+
+ case PARSER_STATE_INITIAL_LF:
+ if (byte == '\r')
+ chat->state = PARSER_STATE_TERMINATOR_CR;
+ else if (byte == '>')
+ chat->state = PARSER_STATE_PROMPT;
+ else
+ chat->state = PARSER_STATE_RESPONSE;
+ break;
+
+ case PARSER_STATE_RESPONSE:
+ if (byte == '\r')
+ chat->state = PARSER_STATE_TERMINATOR_CR;
+ break;
+
+ case PARSER_STATE_TERMINATOR_CR:
+ if (byte == '\n')
+ chat->state = PARSER_STATE_RESPONSE_COMPLETE;
+ else
+ chat->state = PARSER_STATE_IDLE;
+ break;
+
+ case PARSER_STATE_PDU:
+ if (byte == '\r')
+ chat->state = PARSER_STATE_PDU_CR;
+ break;
+
+ case PARSER_STATE_PDU_CR:
+ if (byte == '\n')
+ chat->state = PARSER_STATE_PDU_COMPLETE;
+ break;
+
+ case PARSER_STATE_PROMPT:
+ if (byte == ' ')
+ chat->state = PARSER_STATE_PROMPT_COMPLETE;
+ else
+ chat->state = PARSER_STATE_RESPONSE;
+
+ case PARSER_STATE_RESPONSE_COMPLETE:
+ case PARSER_STATE_PDU_COMPLETE:
+ default:
+ /* This really shouldn't happen */
+ assert(TRUE);
+ return;
+ }
+}
+
+static void new_bytes(GAtChat *p)
+{
+ unsigned int len = ring_buffer_len(p->buf);
+ unsigned int wrap = ring_buffer_len_no_wrap(p->buf);
+ unsigned char *buf = ring_buffer_read_ptr(p->buf, p->read_so_far);
+
+ while (p->read_so_far < len) {
+ parse_char(p, *buf);
+
+ buf += 1;
+ p->read_so_far += 1;
+
+ if (p->read_so_far == wrap) {
+ buf = ring_buffer_read_ptr(p->buf, p->read_so_far);
+ wrap = len;
+ }
+
+ if (p->state == PARSER_STATE_RESPONSE_COMPLETE) {
+ len -= p->read_so_far;
+ wrap -= p->read_so_far;
+
+ have_line(p);
+
+ p->read_so_far = 0;
+ } else if (p->state == PARSER_STATE_PDU_COMPLETE) {
+ len -= p->read_so_far;
+ wrap -= p->read_so_far;
+
+ have_pdu(p);
+
+ p->read_so_far = 0;
+ } else if (p->state == PARSER_STATE_INITIAL_CR) {
+ len -= p->read_so_far - 1;
+ wrap -= p->read_so_far - 1;
+
+ ring_buffer_drain(p->buf, p->read_so_far - 1);
+
+ p->read_so_far = 1;
+ } else if (p->state == PARSER_STATE_PROMPT_COMPLETE) {
+ len -= p->read_so_far;
+ wrap -= p->read_so_far;
+
+ g_at_chat_wakeup_writer(p);
+
+ ring_buffer_drain(p->buf, p->read_so_far);
+
+ p->read_so_far = 0;
+ }
+ }
+
+ if (p->state == PARSER_STATE_IDLE && p->read_so_far > 0) {
+ ring_buffer_drain(p->buf, p->read_so_far);
+ p->read_so_far = 0;
+ }
+}
+
+static gboolean received_data(GIOChannel *channel, GIOCondition cond,
+ gpointer data)
+{
+ unsigned char *buf;
+ GAtChat *chat = data;
+ GIOError err;
+ gsize rbytes;
+ gsize toread;
+ gsize total_read = 0;
+
+ if (cond & G_IO_NVAL)
+ return FALSE;
+
+ /* Regardless of condition, try to read all the data available */
+ do {
+ rbytes = 0;
+
+ toread = ring_buffer_avail_no_wrap(chat->buf);
+
+ /* We're going to start overflowing the buffer
+ * this cannot happen under normal circumstances, so probably
+ * the channel is getting garbage, drop off
+ */
+ if (toread == 0) {
+ if (chat->state == PARSER_STATE_RESPONSE)
+ return FALSE;
+
+ err = G_IO_ERROR_AGAIN;
+ break;
+ }
+
+ buf = ring_buffer_write_ptr(chat->buf);
+
+ err = g_io_channel_read(channel, (char *) buf, toread, &rbytes);
+
+ total_read += rbytes;
+
+ if (rbytes > 0)
+ ring_buffer_write_advance(chat->buf, rbytes);
+
+ } while (err == G_IO_ERROR_NONE && rbytes > 0);
+
+ if (total_read > 0)
+ new_bytes(chat);
+
+ if (cond & (G_IO_HUP | G_IO_ERR))
+ return FALSE;
+
+ if (err == G_IO_ERROR_NONE && rbytes == 0)
+ return FALSE;
+
+ if (err != G_IO_ERROR_NONE && err != G_IO_ERROR_AGAIN)
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean wakeup_no_response(gpointer user)
+{
+ GAtChat *chat = user;
+
+ g_at_chat_finish_command(chat, FALSE, NULL);
+
+ return FALSE;
+}
+
+static gboolean can_write_data(GIOChannel *channel, GIOCondition cond,
+ gpointer data)
+{
+ GAtChat *chat = data;
+ struct at_command *cmd;
+ GIOError err;
+ gsize bytes_written;
+ gsize towrite;
+ gsize len;
+ char *cr;
+ gboolean wakeup_first = FALSE;
+#ifdef WRITE_SCHEDULER_DEBUG
+ int limiter;
+#endif
+
+ if (cond & (G_IO_NVAL | G_IO_HUP | G_IO_ERR))
+ return FALSE;
+
+ /* Grab the first command off the queue and write as
+ * much of it as we can
+ */
+ cmd = g_queue_peek_head(chat->command_queue);
+
+ /* For some reason command queue is empty, cancel write watcher */
+ if (cmd == NULL)
+ return FALSE;
+
+ len = strlen(cmd->cmd);
+
+ /* For some reason write watcher fired, but we've already
+ * written the entire command out to the io channel,
+ * cancel write watcher
+ */
+ if (chat->cmd_bytes_written >= len)
+ return FALSE;
+
+ if (chat->wakeup) {
+ if (!chat->wakeup_timer) {
+ wakeup_first = TRUE;
+ chat->wakeup_timer = g_timer_new();
+
+ } else if (g_timer_elapsed(chat->wakeup_timer, NULL) >
+ chat->inactivity_time)
+ wakeup_first = TRUE;
+ }
+
+ if (chat->cmd_bytes_written == 0 && wakeup_first == TRUE) {
+ cmd = at_command_create(chat->wakeup, NULL, NULL, NULL, NULL);
+
+ if (!cmd)
+ return FALSE;
+
+ g_queue_push_head(chat->command_queue, cmd);
+
+ len = strlen(chat->wakeup);
+
+ g_timeout_add(chat->wakeup_timeout, wakeup_no_response,
+ chat);
+ }
+
+ towrite = len - chat->cmd_bytes_written;
+
+ cr = strchr(cmd->cmd + chat->cmd_bytes_written, '\r');
+
+ if (cr)
+ towrite = cr - (cmd->cmd + chat->cmd_bytes_written) + 1;
+
+#ifdef WRITE_SCHEDULER_DEBUG
+ limiter = towrite;
+
+ if (limiter > 5)
+ limiter = 5;
+#endif
+
+ err = g_io_channel_write(chat->channel,
+ cmd->cmd + chat->cmd_bytes_written,
+#ifdef WRITE_SCHEDULER_DEBUG
+ limiter,
+#else
+ towrite,
+#endif
+ &bytes_written);
+
+ if (err != G_IO_ERROR_NONE) {
+ g_at_chat_shutdown(chat);
+ return FALSE;
+ }
+
+ chat->cmd_bytes_written += bytes_written;
+
+ if (bytes_written < towrite)
+ return TRUE;
+
+ /* Full command submitted, update timer */
+ if (chat->wakeup_timer)
+ g_timer_start(chat->wakeup_timer);
+
+ return FALSE;
+}
+
+static void g_at_chat_wakeup_writer(GAtChat *chat)
+{
+ if (chat->write_watch != 0)
+ return;
+
+ chat->write_watch = g_io_add_watch_full(chat->channel,
+ G_PRIORITY_DEFAULT,
+ G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+ can_write_data, chat,
+ (GDestroyNotify)write_watcher_destroy_notify);
+}
+
+GAtChat *g_at_chat_new(GIOChannel *channel, int flags)
+{
+ GAtChat *chat;
+ GIOFlags io_flags;
+
+ if (!channel)
+ return NULL;
+
+ chat = g_try_new0(GAtChat, 1);
+
+ if (!chat)
+ return chat;
+
+ chat->next_cmd_id = 1;
+ chat->next_notify_id = 1;
+ chat->flags = flags;
+
+ chat->buf = ring_buffer_new(4096);
+
+ if (!chat->buf)
+ goto error;
+
+ chat->command_queue = g_queue_new();
+
+ if (!chat->command_queue)
+ goto error;
+
+ chat->notify_list = g_hash_table_new_full(g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify)at_notify_destroy);
+
+ if (g_io_channel_set_encoding(channel, NULL, NULL) !=
+ G_IO_STATUS_NORMAL)
+ goto error;
+
+ io_flags = g_io_channel_get_flags(channel);
+
+ io_flags |= G_IO_FLAG_NONBLOCK;
+
+ if (g_io_channel_set_flags(channel, io_flags, NULL) !=
+ G_IO_STATUS_NORMAL)
+ goto error;
+
+ g_io_channel_set_close_on_unref(channel, TRUE);
+
+ chat->channel = channel;
+ chat->read_watch = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT,
+ G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+ received_data, chat,
+ (GDestroyNotify)read_watcher_destroy_notify);
+
+ return chat;
+
+error:
+ if (chat->buf)
+ ring_buffer_free(chat->buf);
+
+ if (chat->command_queue)
+ g_queue_free(chat->command_queue);
+
+ if (chat->notify_list)
+ g_hash_table_destroy(chat->notify_list);
+
+ g_free(chat);
+ return NULL;
+}
+
+GAtChat *g_at_chat_ref(GAtChat *chat)
+{
+ if (chat == NULL)
+ return NULL;
+
+ g_atomic_int_inc(&chat->ref_count);
+
+ return chat;
+}
+
+void g_at_chat_unref(GAtChat *chat)
+{
+ gboolean is_zero;
+
+ if (chat == NULL)
+ return;
+
+ is_zero = g_atomic_int_dec_and_test(&chat->ref_count);
+
+ if (is_zero) {
+ g_at_chat_shutdown(chat);
+
+ g_at_chat_cleanup(chat);
+ g_free(chat);
+ }
+}
+
+gboolean g_at_chat_shutdown(GAtChat *chat)
+{
+ if (chat->channel == NULL)
+ return FALSE;
+
+ chat->disconnecting = TRUE;
+
+ if (chat->read_watch)
+ g_source_remove(chat->read_watch);
+
+ if (chat->write_watch)
+ g_source_remove(chat->write_watch);
+
+ return TRUE;
+}
+
+gboolean g_at_chat_set_disconnect_function(GAtChat *chat,
+ GAtDisconnectFunc disconnect, gpointer user_data)
+{
+ if (chat == NULL)
+ return FALSE;
+
+ chat->user_disconnect = disconnect;
+ chat->user_disconnect_data = user_data;
+
+ return TRUE;
+}
+
+guint g_at_chat_send(GAtChat *chat, const char *cmd,
+ const char **prefix_list, GAtResultFunc func,
+ gpointer user_data, GDestroyNotify notify)
+{
+ struct at_command *c;
+
+ if (chat == NULL || chat->command_queue == NULL)
+ return 0;
+
+ c = at_command_create(cmd, prefix_list, func, user_data, notify);
+
+ if (!c)
+ return 0;
+
+ c->id = chat->next_cmd_id++;
+
+ g_queue_push_tail(chat->command_queue, c);
+
+ if (g_queue_get_length(chat->command_queue) == 1)
+ g_at_chat_wakeup_writer(chat);
+
+ return c->id;
+}
+
+gboolean g_at_chat_cancel(GAtChat *chat, guint id)
+{
+ GList *l;
+
+ if (chat == NULL || chat->command_queue == NULL)
+ return FALSE;
+
+ l = g_queue_find_custom(chat->command_queue, GUINT_TO_POINTER(id),
+ at_command_compare_by_id);
+
+ if (!l)
+ return FALSE;
+
+ if (l == g_queue_peek_head(chat->command_queue)) {
+ struct at_command *c = l->data;
+
+ /* We can't actually remove it since it is most likely
+ * already in progress, just null out the callback
+ * so it won't be called
+ */
+ c->callback = NULL;
+ } else {
+ at_command_destroy(l->data);
+ g_queue_remove(chat->command_queue, l->data);
+ }
+
+ return TRUE;
+}
+
+static struct at_notify *at_notify_create(GAtChat *chat, const char *prefix,
+ gboolean pdu)
+{
+ struct at_notify *notify;
+ char *key;
+
+ key = g_strdup(prefix);
+
+ if (!key)
+ return 0;
+
+ notify = g_try_new0(struct at_notify, 1);
+
+ if (!notify) {
+ g_free(key);
+ return 0;
+ }
+
+ notify->pdu = pdu;
+
+ g_hash_table_insert(chat->notify_list, key, notify);
+
+ return notify;
+}
+
+guint g_at_chat_register(GAtChat *chat, const char *prefix,
+ GAtNotifyFunc func, gboolean expect_pdu,
+ gpointer user_data,
+ GDestroyNotify destroy_notify)
+{
+ struct at_notify *notify;
+ struct at_notify_node *node;
+
+ if (chat == NULL || chat->notify_list == NULL)
+ return 0;
+
+ if (func == NULL)
+ return 0;
+
+ if (prefix == NULL || strlen(prefix) == 0)
+ return 0;
+
+ notify = g_hash_table_lookup(chat->notify_list, prefix);
+
+ if (!notify)
+ notify = at_notify_create(chat, prefix, expect_pdu);
+
+ if (!notify || notify->pdu != expect_pdu)
+ return 0;
+
+ node = g_try_new0(struct at_notify_node, 1);
+
+ if (!node)
+ return 0;
+
+ node->id = chat->next_notify_id++;
+ node->callback = func;
+ node->user_data = user_data;
+ node->notify = destroy_notify;
+
+ notify->nodes = g_slist_prepend(notify->nodes, node);
+
+ return node->id;
+}
+
+gboolean g_at_chat_unregister(GAtChat *chat, guint id)
+{
+ GHashTableIter iter;
+ struct at_notify *notify;
+ char *prefix;
+ gpointer key, value;
+ GSList *l;
+
+ if (chat == NULL || chat->notify_list == NULL)
+ return FALSE;
+
+ g_hash_table_iter_init(&iter, chat->notify_list);
+
+ while (g_hash_table_iter_next(&iter, &key, &value)) {
+ prefix = key;
+ notify = value;
+
+ l = g_slist_find_custom(notify->nodes, GUINT_TO_POINTER(id),
+ at_notify_node_compare_by_id);
+
+ if (!l)
+ continue;
+
+ at_notify_node_destroy(l->data);
+ notify->nodes = g_slist_remove(notify->nodes, l->data);
+
+ if (notify->nodes == NULL)
+ g_hash_table_iter_remove(&iter);
+
+ return TRUE;
+ }
+
+ return TRUE;
+}
+
+gboolean g_at_chat_set_wakeup_command(GAtChat *chat, const char *cmd,
+ unsigned int timeout, unsigned int msec)
+{
+ if (chat == NULL)
+ return FALSE;
+
+ if (chat->wakeup)
+ g_free(chat->wakeup);
+
+ chat->wakeup = g_strdup(cmd);
+ chat->inactivity_time = (gdouble)msec / 1000;
+ chat->wakeup_timeout = timeout;
+
+ return TRUE;
+}