| /* |
| * |
| * AT chat library with GLib integration |
| * |
| * Copyright (C) 2008-2011 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 <unistd.h> |
| #include <string.h> |
| #include <assert.h> |
| #include <ctype.h> |
| #include <errno.h> |
| |
| #include <glib.h> |
| |
| #include "ringbuffer.h" |
| #include "gatchat.h" |
| #include "gatio.h" |
| |
| /* #define WRITE_SCHEDULER_DEBUG 1 */ |
| |
| #define COMMAND_FLAG_EXPECT_PDU 0x1 |
| #define COMMAND_FLAG_EXPECT_SHORT_PROMPT 0x2 |
| |
| struct at_chat; |
| static void chat_wakeup_writer(struct at_chat *chat); |
| |
| static const char *none_prefix[] = { NULL }; |
| |
| struct at_command { |
| char *cmd; |
| char **prefixes; |
| guint flags; |
| guint id; |
| guint gid; |
| GAtResultFunc callback; |
| GAtNotifyFunc listing; |
| gpointer user_data; |
| GDestroyNotify notify; |
| }; |
| |
| struct at_notify_node { |
| guint id; |
| guint gid; |
| GAtNotifyFunc callback; |
| gpointer user_data; |
| GDestroyNotify notify; |
| gboolean destroyed; |
| }; |
| |
| typedef gboolean (*node_remove_func)(struct at_notify_node *node, |
| gpointer user_data); |
| |
| struct at_notify { |
| GSList *nodes; |
| gboolean pdu; |
| }; |
| |
| struct at_chat { |
| gint ref_count; /* Ref count */ |
| guint next_cmd_id; /* Next command id */ |
| guint next_notify_id; /* Next notify id */ |
| guint next_gid; /* Next group id */ |
| GAtIO *io; /* AT IO */ |
| 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 */ |
| guint read_so_far; /* Number of bytes processed */ |
| gboolean suspended; /* Are we suspended? */ |
| GAtDebugFunc debugf; /* debugging output function */ |
| gpointer debug_data; /* Data to pass to debug func */ |
| char *pdu_notify; /* Unsolicited Resp w/ PDU */ |
| GSList *response_lines; /* char * lines of the response */ |
| char *wakeup; /* command sent to wakeup modem */ |
| gint timeout_source; |
| gdouble inactivity_time; /* Period of inactivity */ |
| guint wakeup_timeout; /* How long to wait for resp */ |
| GTimer *wakeup_timer; /* Keep track of elapsed time */ |
| GAtSyntax *syntax; |
| gboolean destroyed; /* Re-entrancy guard */ |
| gboolean in_read_handler; /* Re-entrancy guard */ |
| gboolean in_notify; |
| GSList *terminator_list; /* Non-standard terminator */ |
| guint16 terminator_blacklist; /* Blacklisted terinators */ |
| }; |
| |
| struct _GAtChat { |
| gint ref_count; |
| struct at_chat *parent; |
| guint group; |
| GAtChat *slave; |
| }; |
| |
| struct terminator_info { |
| char *terminator; |
| int len; |
| gboolean success; |
| }; |
| |
| static gboolean node_is_destroyed(struct at_notify_node *node, gpointer user) |
| { |
| return node->destroyed; |
| } |
| |
| 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(gpointer data, gpointer user_data) |
| { |
| struct at_notify_node *node = data; |
| |
| if (node->notify) |
| node->notify(node->user_data); |
| |
| g_free(node); |
| } |
| |
| static void at_notify_destroy(gpointer user_data) |
| { |
| struct at_notify *notify = user_data; |
| |
| g_slist_foreach(notify->nodes, at_notify_node_destroy, NULL); |
| g_slist_free(notify->nodes); |
| 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 gboolean at_chat_unregister_all(struct at_chat *chat, |
| gboolean mark_only, |
| node_remove_func func, |
| gpointer userdata) |
| { |
| GHashTableIter iter; |
| struct at_notify *notify; |
| struct at_notify_node *node; |
| gpointer key, value; |
| GSList *p; |
| GSList *c; |
| GSList *t; |
| |
| if (chat->notify_list == NULL) |
| return FALSE; |
| |
| g_hash_table_iter_init(&iter, chat->notify_list); |
| |
| while (g_hash_table_iter_next(&iter, &key, &value)) { |
| notify = value; |
| |
| p = NULL; |
| c = notify->nodes; |
| |
| while (c) { |
| node = c->data; |
| |
| if (func(node, userdata) != TRUE) { |
| p = c; |
| c = c->next; |
| continue; |
| } |
| |
| if (mark_only) { |
| node->destroyed = TRUE; |
| p = c; |
| c = c->next; |
| continue; |
| } |
| |
| if (p) |
| p->next = c->next; |
| else |
| notify->nodes = c->next; |
| |
| at_notify_node_destroy(node, NULL); |
| |
| t = c; |
| c = c->next; |
| g_slist_free_1(t); |
| } |
| |
| if (notify->nodes == NULL) |
| g_hash_table_iter_remove(&iter); |
| } |
| |
| return TRUE; |
| } |
| |
| static struct at_command *at_command_create(guint gid, const char *cmd, |
| const char **prefix_list, |
| guint flags, |
| GAtNotifyFunc listing, |
| GAtResultFunc func, |
| gpointer user_data, |
| GDestroyNotify notify, |
| gboolean wakeup) |
| { |
| 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 == NULL) |
| return 0; |
| |
| len = strlen(cmd); |
| c->cmd = g_try_new(char, len + 2); |
| if (c->cmd == NULL) { |
| 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 (wakeup == FALSE) { |
| if (strchr(cmd, '\r')) |
| c->cmd[len] = 26; |
| else |
| c->cmd[len] = '\r'; |
| |
| len += 1; |
| } |
| |
| c->cmd[len] = '\0'; |
| |
| c->gid = gid; |
| c->flags = flags; |
| c->prefixes = prefixes; |
| c->callback = func; |
| c->listing = listing; |
| 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 free_terminator(gpointer pointer) |
| { |
| struct terminator_info *info = pointer; |
| g_free(info->terminator); |
| info->terminator = NULL; |
| g_free(info); |
| info = NULL; |
| } |
| |
| static void chat_cleanup(struct at_chat *chat) |
| { |
| struct at_command *c; |
| |
| /* 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_free_full(chat->response_lines, g_free); |
| 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; |
| } |
| |
| if (chat->timeout_source) { |
| g_source_remove(chat->timeout_source); |
| chat->timeout_source = 0; |
| } |
| |
| g_at_syntax_unref(chat->syntax); |
| chat->syntax = NULL; |
| |
| if (chat->terminator_list) { |
| g_slist_free_full(chat->terminator_list, free_terminator); |
| chat->terminator_list = NULL; |
| } |
| } |
| |
| static void io_disconnect(gpointer user_data) |
| { |
| struct at_chat *chat = user_data; |
| |
| chat_cleanup(chat); |
| g_at_io_unref(chat->io); |
| chat->io = NULL; |
| |
| if (chat->user_disconnect) |
| chat->user_disconnect(chat->user_disconnect_data); |
| } |
| |
| 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 at_chat_match_notify(struct at_chat *chat, char *line) |
| { |
| GHashTableIter iter; |
| struct at_notify *notify; |
| 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; |
| |
| chat->in_notify = TRUE; |
| |
| while (g_hash_table_iter_next(&iter, &key, &value)) { |
| notify = value; |
| |
| if (!g_str_has_prefix(line, key)) |
| continue; |
| |
| if (notify->pdu) { |
| chat->pdu_notify = line; |
| |
| if (chat->syntax->set_hint) |
| chat->syntax->set_hint(chat->syntax, |
| G_AT_SYNTAX_EXPECT_PDU); |
| return TRUE; |
| } |
| |
| if (result.lines == NULL) |
| result.lines = g_slist_prepend(NULL, line); |
| |
| g_slist_foreach(notify->nodes, at_notify_call_callback, |
| &result); |
| ret = TRUE; |
| } |
| |
| chat->in_notify = FALSE; |
| |
| if (ret) { |
| g_slist_free(result.lines); |
| g_free(line); |
| |
| at_chat_unregister_all(chat, FALSE, node_is_destroyed, NULL); |
| } |
| |
| return ret; |
| } |
| |
| static void at_chat_finish_command(struct at_chat *p, gboolean ok, char *final) |
| { |
| struct at_command *cmd = g_queue_pop_head(p->command_queue); |
| GSList *response_lines; |
| |
| /* Cannot happen, but lets be paranoid */ |
| if (cmd == NULL) |
| return; |
| |
| p->cmd_bytes_written = 0; |
| |
| if (g_queue_peek_head(p->command_queue)) |
| chat_wakeup_writer(p); |
| |
| response_lines = p->response_lines; |
| p->response_lines = NULL; |
| |
| if (cmd->callback) { |
| GAtResult result; |
| |
| response_lines = g_slist_reverse(response_lines); |
| |
| result.final_or_pdu = final; |
| result.lines = response_lines; |
| |
| cmd->callback(ok, &result, cmd->user_data); |
| } |
| |
| g_slist_free_full(response_lines, g_free); |
| |
| g_free(final); |
| at_command_destroy(cmd); |
| } |
| |
| static struct terminator_info terminator_table[] = { |
| { "OK", -1, TRUE }, |
| { "ERROR", -1, FALSE }, |
| { "NO DIALTONE", -1, FALSE }, |
| { "BUSY", -1, FALSE }, |
| { "NO CARRIER", -1, FALSE }, |
| { "CONNECT", 7, TRUE }, |
| { "NO ANSWER", -1, FALSE }, |
| { "+CMS ERROR:", 11, FALSE }, |
| { "+CME ERROR:", 11, FALSE }, |
| { "+EXT ERROR:", 11, FALSE } |
| }; |
| |
| static void at_chat_add_terminator(struct at_chat *chat, char *terminator, |
| int len, gboolean success) |
| { |
| struct terminator_info *info = g_new0(struct terminator_info, 1); |
| info->terminator = g_strdup(terminator); |
| info->len = len; |
| info->success = success; |
| chat->terminator_list = g_slist_prepend(chat->terminator_list, info); |
| } |
| |
| static void at_chat_blacklist_terminator(struct at_chat *chat, |
| GAtChatTerminator terminator) |
| { |
| chat->terminator_blacklist |= 1 << terminator; |
| } |
| |
| static gboolean check_terminator(struct terminator_info *info, char *line) |
| { |
| if (info->len == -1 && !strcmp(line, info->terminator)) |
| return TRUE; |
| |
| if (info->len > 0 && !strncmp(line, info->terminator, info->len)) |
| return TRUE; |
| |
| return FALSE; |
| } |
| |
| static gboolean at_chat_handle_command_response(struct at_chat *p, |
| struct at_command *cmd, |
| char *line) |
| { |
| int i; |
| int size = sizeof(terminator_table) / sizeof(struct terminator_info); |
| int hint; |
| GSList *l; |
| |
| for (i = 0; i < size; i++) { |
| struct terminator_info *info = &terminator_table[i]; |
| if (check_terminator(info, line) && |
| (p->terminator_blacklist & 1 << i) == 0) { |
| at_chat_finish_command(p, info->success, line); |
| return TRUE; |
| } |
| } |
| |
| for (l = p->terminator_list; l; l = l->next) { |
| struct terminator_info *info = l->data; |
| if (check_terminator(info, line)) { |
| at_chat_finish_command(p, info->success, line); |
| return TRUE; |
| } |
| } |
| |
| if (cmd->prefixes) { |
| int n; |
| |
| for (n = 0; cmd->prefixes[n]; n++) |
| if (g_str_has_prefix(line, cmd->prefixes[n])) |
| goto out; |
| |
| return FALSE; |
| } |
| |
| out: |
| if (cmd->listing && (cmd->flags & COMMAND_FLAG_EXPECT_PDU)) |
| hint = G_AT_SYNTAX_EXPECT_PDU; |
| else |
| hint = G_AT_SYNTAX_EXPECT_MULTILINE; |
| |
| if (p->syntax->set_hint) |
| p->syntax->set_hint(p->syntax, hint); |
| |
| if (cmd->listing && (cmd->flags & COMMAND_FLAG_EXPECT_PDU)) { |
| p->pdu_notify = line; |
| return TRUE; |
| } |
| |
| if (cmd->listing) { |
| GAtResult result; |
| |
| result.lines = g_slist_prepend(NULL, line); |
| result.final_or_pdu = NULL; |
| |
| cmd->listing(&result, cmd->user_data); |
| |
| g_slist_free(result.lines); |
| g_free(line); |
| } else |
| p->response_lines = g_slist_prepend(p->response_lines, line); |
| |
| return TRUE; |
| } |
| |
| static void have_line(struct at_chat *p, char *str) |
| { |
| /* We're not going to copy terminal <CR><LF> */ |
| struct at_command *cmd; |
| |
| if (str == NULL) |
| return; |
| |
| /* Check for echo, this should not happen, but lets be paranoid */ |
| if (!strncmp(str, "AT", 2)) |
| goto done; |
| |
| cmd = g_queue_peek_head(p->command_queue); |
| |
| if (cmd && p->cmd_bytes_written > 0) { |
| char c = cmd->cmd[p->cmd_bytes_written - 1]; |
| |
| /* We check that we have submitted a terminator, in which case |
| * a command might have failed or completed successfully |
| * |
| * In the generic case, \r is at the end of the command, so we |
| * know the entire command has been submitted. In the case of |
| * commands like CMGS, every \r or Ctrl-Z might result in a |
| * final response from the modem, so we check this as well. |
| */ |
| if ((c == '\r' || c == 26) && |
| at_chat_handle_command_response(p, cmd, str)) |
| return; |
| } |
| |
| if (at_chat_match_notify(p, str) == TRUE) |
| return; |
| |
| done: |
| /* No matches & no commands active, ignore line */ |
| g_free(str); |
| } |
| |
| static void have_notify_pdu(struct at_chat *p, char *pdu, GAtResult *result) |
| { |
| GHashTableIter iter; |
| struct at_notify *notify; |
| char *prefix; |
| gpointer key, value; |
| gboolean called = FALSE; |
| |
| p->in_notify = TRUE; |
| |
| 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); |
| called = TRUE; |
| } |
| |
| p->in_notify = FALSE; |
| |
| if (called) |
| at_chat_unregister_all(p, FALSE, node_is_destroyed, NULL); |
| } |
| |
| static void have_pdu(struct at_chat *p, char *pdu) |
| { |
| struct at_command *cmd; |
| GAtResult result; |
| gboolean listing_pdu = FALSE; |
| |
| if (pdu == NULL) |
| goto error; |
| |
| result.lines = g_slist_prepend(NULL, p->pdu_notify); |
| result.final_or_pdu = pdu; |
| |
| cmd = g_queue_peek_head(p->command_queue); |
| |
| if (cmd && (cmd->flags & COMMAND_FLAG_EXPECT_PDU) && |
| p->cmd_bytes_written > 0) { |
| char c = cmd->cmd[p->cmd_bytes_written - 1]; |
| |
| if (c == '\r') |
| listing_pdu = TRUE; |
| } |
| |
| if (listing_pdu) { |
| cmd->listing(&result, cmd->user_data); |
| |
| if (p->syntax->set_hint) |
| p->syntax->set_hint(p->syntax, |
| G_AT_SYNTAX_EXPECT_MULTILINE); |
| } else |
| have_notify_pdu(p, pdu, &result); |
| |
| g_slist_free(result.lines); |
| |
| error: |
| g_free(p->pdu_notify); |
| p->pdu_notify = NULL; |
| |
| if (pdu) |
| g_free(pdu); |
| } |
| |
| static char *extract_line(struct at_chat *p, struct ring_buffer *rbuf) |
| { |
| unsigned int wrap = ring_buffer_len_no_wrap(rbuf); |
| unsigned int pos = 0; |
| unsigned char *buf = ring_buffer_read_ptr(rbuf, pos); |
| gboolean in_string = FALSE; |
| int strip_front = 0; |
| int line_length = 0; |
| char *line; |
| |
| while (pos < p->read_so_far) { |
| if (in_string == FALSE && (*buf == '\r' || *buf == '\n')) { |
| if (!line_length) |
| strip_front += 1; |
| else |
| break; |
| } else { |
| if (*buf == '"') |
| in_string = !in_string; |
| |
| line_length += 1; |
| } |
| |
| buf += 1; |
| pos += 1; |
| |
| if (pos == wrap) |
| buf = ring_buffer_read_ptr(rbuf, pos); |
| } |
| |
| line = g_try_new(char, line_length + 1); |
| if (line == NULL) { |
| ring_buffer_drain(rbuf, p->read_so_far); |
| return NULL; |
| } |
| |
| ring_buffer_drain(rbuf, strip_front); |
| ring_buffer_read(rbuf, line, line_length); |
| ring_buffer_drain(rbuf, p->read_so_far - strip_front - line_length); |
| |
| line[line_length] = '\0'; |
| |
| return line; |
| } |
| |
| static void new_bytes(struct ring_buffer *rbuf, gpointer user_data) |
| { |
| struct at_chat *p = user_data; |
| unsigned int len = ring_buffer_len(rbuf); |
| unsigned int wrap = ring_buffer_len_no_wrap(rbuf); |
| unsigned char *buf = ring_buffer_read_ptr(rbuf, p->read_so_far); |
| |
| GAtSyntaxResult result; |
| |
| p->in_read_handler = TRUE; |
| |
| while (p->suspended == FALSE && (p->read_so_far < len)) { |
| gsize rbytes = MIN(len - p->read_so_far, wrap - p->read_so_far); |
| result = p->syntax->feed(p->syntax, (char *)buf, &rbytes); |
| |
| buf += rbytes; |
| p->read_so_far += rbytes; |
| |
| if (p->read_so_far == wrap) { |
| buf = ring_buffer_read_ptr(rbuf, p->read_so_far); |
| wrap = len; |
| } |
| |
| if (result == G_AT_SYNTAX_RESULT_UNSURE) |
| continue; |
| |
| switch (result) { |
| case G_AT_SYNTAX_RESULT_LINE: |
| case G_AT_SYNTAX_RESULT_MULTILINE: |
| have_line(p, extract_line(p, rbuf)); |
| break; |
| |
| case G_AT_SYNTAX_RESULT_PDU: |
| have_pdu(p, extract_line(p, rbuf)); |
| break; |
| |
| case G_AT_SYNTAX_RESULT_PROMPT: |
| chat_wakeup_writer(p); |
| ring_buffer_drain(rbuf, p->read_so_far); |
| break; |
| |
| default: |
| ring_buffer_drain(rbuf, p->read_so_far); |
| break; |
| } |
| |
| len -= p->read_so_far; |
| wrap -= p->read_so_far; |
| p->read_so_far = 0; |
| } |
| |
| p->in_read_handler = FALSE; |
| |
| if (p->destroyed) |
| g_free(p); |
| } |
| |
| static void wakeup_cb(gboolean ok, GAtResult *result, gpointer user_data) |
| { |
| struct at_chat *chat = user_data; |
| |
| if (ok == FALSE) |
| return; |
| |
| if (chat->debugf) |
| chat->debugf("Finally woke up the modem\n", chat->debug_data); |
| |
| g_source_remove(chat->timeout_source); |
| chat->timeout_source = 0; |
| } |
| |
| static gboolean wakeup_no_response(gpointer user_data) |
| { |
| struct at_chat *chat = user_data; |
| struct at_command *cmd = g_queue_peek_head(chat->command_queue); |
| |
| if (chat->debugf) |
| chat->debugf("Wakeup got no response\n", chat->debug_data); |
| |
| if (cmd == NULL) |
| return FALSE; |
| |
| at_chat_finish_command(chat, FALSE, NULL); |
| |
| cmd = at_command_create(0, chat->wakeup, none_prefix, 0, |
| NULL, wakeup_cb, chat, NULL, TRUE); |
| if (cmd == NULL) { |
| chat->timeout_source = 0; |
| return FALSE; |
| } |
| |
| g_queue_push_head(chat->command_queue, cmd); |
| |
| return TRUE; |
| } |
| |
| static gboolean can_write_data(gpointer data) |
| { |
| struct at_chat *chat = data; |
| struct at_command *cmd; |
| gsize bytes_written; |
| gsize towrite; |
| gsize len; |
| char *cr; |
| gboolean wakeup_first = FALSE; |
| #ifdef WRITE_SCHEDULER_DEBUG |
| int limiter; |
| #endif |
| |
| /* 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 == NULL) { |
| 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(0, chat->wakeup, none_prefix, 0, |
| NULL, wakeup_cb, chat, NULL, TRUE); |
| if (cmd == NULL) |
| return FALSE; |
| |
| g_queue_push_head(chat->command_queue, cmd); |
| |
| len = strlen(chat->wakeup); |
| |
| chat->timeout_source = 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 |
| |
| bytes_written = g_at_io_write(chat->io, |
| cmd->cmd + chat->cmd_bytes_written, |
| #ifdef WRITE_SCHEDULER_DEBUG |
| limiter |
| #else |
| towrite |
| #endif |
| ); |
| |
| if (bytes_written == 0) |
| return FALSE; |
| |
| chat->cmd_bytes_written += bytes_written; |
| |
| if (bytes_written < towrite) |
| return TRUE; |
| |
| /* |
| * If we're expecting a short prompt, set the hint for all lines |
| * sent to the modem except the last |
| */ |
| if ((cmd->flags & COMMAND_FLAG_EXPECT_SHORT_PROMPT) && |
| chat->cmd_bytes_written < len && |
| chat->syntax->set_hint) |
| chat->syntax->set_hint(chat->syntax, |
| G_AT_SYNTAX_EXPECT_SHORT_PROMPT); |
| |
| /* Full command submitted, update timer */ |
| if (chat->wakeup_timer) |
| g_timer_start(chat->wakeup_timer); |
| |
| return FALSE; |
| } |
| |
| static void chat_wakeup_writer(struct at_chat *chat) |
| { |
| g_at_io_set_write_handler(chat->io, can_write_data, chat); |
| } |
| |
| static void at_chat_suspend(struct at_chat *chat) |
| { |
| chat->suspended = TRUE; |
| |
| g_at_io_set_write_handler(chat->io, NULL, NULL); |
| g_at_io_set_read_handler(chat->io, NULL, NULL); |
| g_at_io_set_debug(chat->io, NULL, NULL); |
| } |
| |
| static void at_chat_resume(struct at_chat *chat) |
| { |
| chat->suspended = FALSE; |
| |
| if (g_at_io_get_channel(chat->io) == NULL) { |
| io_disconnect(chat); |
| return; |
| } |
| |
| g_at_io_set_disconnect_function(chat->io, io_disconnect, chat); |
| |
| g_at_io_set_debug(chat->io, chat->debugf, chat->debug_data); |
| g_at_io_set_read_handler(chat->io, new_bytes, chat); |
| |
| if (g_queue_get_length(chat->command_queue) > 0) |
| chat_wakeup_writer(chat); |
| } |
| |
| static void at_chat_unref(struct at_chat *chat) |
| { |
| gboolean is_zero; |
| |
| is_zero = g_atomic_int_dec_and_test(&chat->ref_count); |
| |
| if (is_zero == FALSE) |
| return; |
| |
| if (chat->io) { |
| at_chat_suspend(chat); |
| g_at_io_unref(chat->io); |
| chat->io = NULL; |
| chat_cleanup(chat); |
| } |
| |
| if (chat->in_read_handler) |
| chat->destroyed = TRUE; |
| else |
| g_free(chat); |
| } |
| |
| static gboolean at_chat_set_disconnect_function(struct at_chat *chat, |
| GAtDisconnectFunc disconnect, |
| gpointer user_data) |
| { |
| chat->user_disconnect = disconnect; |
| chat->user_disconnect_data = user_data; |
| |
| return TRUE; |
| } |
| |
| static gboolean at_chat_set_debug(struct at_chat *chat, |
| GAtDebugFunc func, gpointer user_data) |
| { |
| |
| chat->debugf = func; |
| chat->debug_data = user_data; |
| |
| if (chat->io) |
| g_at_io_set_debug(chat->io, func, user_data); |
| |
| return TRUE; |
| } |
| |
| static gboolean at_chat_set_wakeup_command(struct at_chat *chat, |
| const char *cmd, |
| unsigned int timeout, |
| unsigned int msec) |
| { |
| if (chat->wakeup) |
| g_free(chat->wakeup); |
| |
| chat->wakeup = g_strdup(cmd); |
| chat->inactivity_time = (gdouble)msec / 1000; |
| chat->wakeup_timeout = timeout; |
| |
| return TRUE; |
| } |
| |
| static guint at_chat_send_common(struct at_chat *chat, guint gid, |
| const char *cmd, |
| const char **prefix_list, |
| guint flags, |
| GAtNotifyFunc listing, |
| GAtResultFunc func, |
| gpointer user_data, |
| GDestroyNotify notify) |
| { |
| struct at_command *c; |
| |
| if (chat == NULL || chat->command_queue == NULL) |
| return 0; |
| |
| c = at_command_create(gid, cmd, prefix_list, flags, listing, func, |
| user_data, notify, FALSE); |
| if (c == NULL) |
| 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) |
| chat_wakeup_writer(chat); |
| |
| return c->id; |
| } |
| |
| static struct at_notify *at_notify_create(struct at_chat *chat, |
| const char *prefix, |
| gboolean pdu) |
| { |
| struct at_notify *notify; |
| char *key; |
| |
| key = g_strdup(prefix); |
| if (key == NULL) |
| return 0; |
| |
| notify = g_try_new0(struct at_notify, 1); |
| if (notify == NULL) { |
| g_free(key); |
| return 0; |
| } |
| |
| notify->pdu = pdu; |
| |
| g_hash_table_insert(chat->notify_list, key, notify); |
| |
| return notify; |
| } |
| |
| static gboolean at_chat_cancel(struct at_chat *chat, guint group, guint id) |
| { |
| GList *l; |
| struct at_command *c; |
| |
| if (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 == NULL) |
| return FALSE; |
| |
| c = l->data; |
| |
| if (c->gid != group) |
| return FALSE; |
| |
| if (c == g_queue_peek_head(chat->command_queue) && |
| chat->cmd_bytes_written > 0) { |
| /* 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(c); |
| g_queue_remove(chat->command_queue, c); |
| } |
| |
| return TRUE; |
| } |
| |
| static gboolean at_chat_cancel_group(struct at_chat *chat, guint group) |
| { |
| int n = 0; |
| struct at_command *c; |
| |
| if (chat->command_queue == NULL) |
| return FALSE; |
| |
| while ((c = g_queue_peek_nth(chat->command_queue, n)) != NULL) { |
| if (c->id == 0 || c->gid != group) { |
| n += 1; |
| continue; |
| } |
| |
| if (n == 0 && chat->cmd_bytes_written > 0) { |
| c->callback = NULL; |
| n += 1; |
| continue; |
| } |
| |
| at_command_destroy(c); |
| g_queue_remove(chat->command_queue, c); |
| } |
| |
| return TRUE; |
| } |
| |
| static gpointer at_chat_get_userdata(struct at_chat *chat, |
| guint group, guint id) |
| { |
| GList *l; |
| struct at_command *c; |
| |
| if (chat->command_queue == NULL) |
| return NULL; |
| |
| l = g_queue_find_custom(chat->command_queue, GUINT_TO_POINTER(id), |
| at_command_compare_by_id); |
| |
| if (l == NULL) |
| return NULL; |
| |
| c = l->data; |
| |
| if (c->gid != group) |
| return NULL; |
| |
| return c->user_data; |
| } |
| |
| static guint at_chat_register(struct at_chat *chat, guint group, |
| 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->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 == NULL) |
| notify = at_notify_create(chat, prefix, expect_pdu); |
| |
| if (notify == NULL || notify->pdu != expect_pdu) |
| return 0; |
| |
| node = g_try_new0(struct at_notify_node, 1); |
| if (node == NULL) |
| return 0; |
| |
| node->id = chat->next_notify_id++; |
| node->gid = group; |
| node->callback = func; |
| node->user_data = user_data; |
| node->notify = destroy_notify; |
| |
| notify->nodes = g_slist_prepend(notify->nodes, node); |
| |
| return node->id; |
| } |
| |
| static gboolean at_chat_unregister(struct at_chat *chat, gboolean mark_only, |
| guint group, guint id) |
| { |
| GHashTableIter iter; |
| struct at_notify *notify; |
| struct at_notify_node *node; |
| gpointer key, value; |
| GSList *l; |
| |
| if (chat->notify_list == NULL) |
| return FALSE; |
| |
| g_hash_table_iter_init(&iter, chat->notify_list); |
| |
| while (g_hash_table_iter_next(&iter, &key, &value)) { |
| notify = value; |
| |
| l = g_slist_find_custom(notify->nodes, GUINT_TO_POINTER(id), |
| at_notify_node_compare_by_id); |
| |
| if (l == NULL) |
| continue; |
| |
| node = l->data; |
| |
| if (node->gid != group) |
| return FALSE; |
| |
| if (mark_only) { |
| node->destroyed = TRUE; |
| return TRUE; |
| } |
| |
| at_notify_node_destroy(node, NULL); |
| notify->nodes = g_slist_remove(notify->nodes, node); |
| |
| if (notify->nodes == NULL) |
| g_hash_table_iter_remove(&iter); |
| |
| return TRUE; |
| } |
| |
| return FALSE; |
| } |
| |
| static gboolean node_compare_by_group(struct at_notify_node *node, |
| gpointer userdata) |
| { |
| guint group = GPOINTER_TO_UINT(userdata); |
| |
| if (node->gid == group) |
| return TRUE; |
| |
| return FALSE; |
| } |
| |
| static struct at_chat *create_chat(GIOChannel *channel, GIOFlags flags, |
| GAtSyntax *syntax) |
| { |
| struct at_chat *chat; |
| |
| if (channel == NULL) |
| return NULL; |
| |
| if (syntax == NULL) |
| return NULL; |
| |
| chat = g_try_new0(struct at_chat, 1); |
| if (chat == NULL) |
| return chat; |
| |
| chat->ref_count = 1; |
| chat->next_cmd_id = 1; |
| chat->next_notify_id = 1; |
| chat->debugf = NULL; |
| |
| if (flags & G_IO_FLAG_NONBLOCK) |
| chat->io = g_at_io_new(channel); |
| else |
| chat->io = g_at_io_new_blocking(channel); |
| |
| if (chat->io == NULL) |
| goto error; |
| |
| g_at_io_set_disconnect_function(chat->io, io_disconnect, chat); |
| |
| chat->command_queue = g_queue_new(); |
| if (chat->command_queue == NULL) |
| goto error; |
| |
| chat->notify_list = g_hash_table_new_full(g_str_hash, g_str_equal, |
| g_free, at_notify_destroy); |
| |
| g_at_io_set_read_handler(chat->io, new_bytes, chat); |
| |
| chat->syntax = g_at_syntax_ref(syntax); |
| |
| return chat; |
| |
| error: |
| g_at_io_unref(chat->io); |
| |
| 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; |
| } |
| |
| static GAtChat *g_at_chat_new_common(GIOChannel *channel, GIOFlags flags, |
| GAtSyntax *syntax) |
| { |
| GAtChat *chat; |
| |
| chat = g_try_new0(GAtChat, 1); |
| if (chat == NULL) |
| return NULL; |
| |
| chat->parent = create_chat(channel, flags, syntax); |
| if (chat->parent == NULL) { |
| g_free(chat); |
| return NULL; |
| } |
| |
| chat->group = chat->parent->next_gid++; |
| chat->ref_count = 1; |
| |
| return chat; |
| } |
| |
| GAtChat *g_at_chat_new(GIOChannel *channel, GAtSyntax *syntax) |
| { |
| return g_at_chat_new_common(channel, G_IO_FLAG_NONBLOCK, syntax); |
| } |
| |
| GAtChat *g_at_chat_new_blocking(GIOChannel *channel, GAtSyntax *syntax) |
| { |
| return g_at_chat_new_common(channel, 0, syntax); |
| } |
| |
| GAtChat *g_at_chat_clone(GAtChat *clone) |
| { |
| GAtChat *chat; |
| |
| if (clone == NULL) |
| return NULL; |
| |
| chat = g_try_new0(GAtChat, 1); |
| if (chat == NULL) |
| return NULL; |
| |
| chat->parent = clone->parent; |
| chat->group = chat->parent->next_gid++; |
| chat->ref_count = 1; |
| g_atomic_int_inc(&chat->parent->ref_count); |
| |
| if (clone->slave != NULL) |
| chat->slave = g_at_chat_clone(clone->slave); |
| |
| return chat; |
| } |
| |
| GAtChat *g_at_chat_set_slave(GAtChat *chat, GAtChat *slave) |
| { |
| if (chat == NULL) |
| return NULL; |
| |
| if (chat->slave != NULL) |
| g_at_chat_unref(chat->slave); |
| |
| if (slave != NULL) |
| chat->slave = g_at_chat_ref(slave); |
| else |
| chat->slave = NULL; |
| |
| return chat->slave; |
| } |
| |
| GAtChat *g_at_chat_get_slave(GAtChat *chat) |
| { |
| if (chat == NULL) |
| return NULL; |
| |
| return chat->slave; |
| } |
| |
| GIOChannel *g_at_chat_get_channel(GAtChat *chat) |
| { |
| if (chat == NULL || chat->parent->io == NULL) |
| return NULL; |
| |
| return g_at_io_get_channel(chat->parent->io); |
| } |
| |
| GAtIO *g_at_chat_get_io(GAtChat *chat) |
| { |
| if (chat == NULL) |
| return NULL; |
| |
| return chat->parent->io; |
| } |
| |
| 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_suspend(GAtChat *chat) |
| { |
| if (chat == NULL) |
| return; |
| |
| at_chat_suspend(chat->parent); |
| } |
| |
| void g_at_chat_resume(GAtChat *chat) |
| { |
| if (chat == NULL) |
| return; |
| |
| at_chat_resume(chat->parent); |
| } |
| |
| 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 == FALSE) |
| return; |
| |
| if (chat->slave != NULL) |
| g_at_chat_unref(chat->slave); |
| |
| at_chat_cancel_group(chat->parent, chat->group); |
| g_at_chat_unregister_all(chat); |
| at_chat_unref(chat->parent); |
| |
| g_free(chat); |
| } |
| |
| gboolean g_at_chat_set_disconnect_function(GAtChat *chat, |
| GAtDisconnectFunc disconnect, gpointer user_data) |
| { |
| if (chat == NULL || chat->group != 0) |
| return FALSE; |
| |
| return at_chat_set_disconnect_function(chat->parent, disconnect, |
| user_data); |
| } |
| |
| gboolean g_at_chat_set_debug(GAtChat *chat, |
| GAtDebugFunc func, gpointer user_data) |
| { |
| |
| if (chat == NULL || chat->group != 0) |
| return FALSE; |
| |
| return at_chat_set_debug(chat->parent, func, user_data); |
| } |
| |
| void g_at_chat_add_terminator(GAtChat *chat, char *terminator, |
| int len, gboolean success) |
| { |
| if (chat == NULL || chat->group != 0) |
| return; |
| |
| at_chat_add_terminator(chat->parent, terminator, len, success); |
| } |
| |
| void g_at_chat_blacklist_terminator(GAtChat *chat, |
| GAtChatTerminator terminator) |
| { |
| if (chat == NULL || chat->group != 0) |
| return; |
| |
| at_chat_blacklist_terminator(chat->parent, terminator); |
| } |
| |
| gboolean g_at_chat_set_wakeup_command(GAtChat *chat, const char *cmd, |
| unsigned int timeout, unsigned int msec) |
| { |
| if (chat == NULL || chat->group != 0) |
| return FALSE; |
| |
| return at_chat_set_wakeup_command(chat->parent, cmd, timeout, msec); |
| } |
| |
| guint g_at_chat_send(GAtChat *chat, const char *cmd, |
| const char **prefix_list, GAtResultFunc func, |
| gpointer user_data, GDestroyNotify notify) |
| { |
| return at_chat_send_common(chat->parent, chat->group, |
| cmd, prefix_list, 0, NULL, |
| func, user_data, notify); |
| } |
| |
| guint g_at_chat_send_listing(GAtChat *chat, const char *cmd, |
| const char **prefix_list, |
| GAtNotifyFunc listing, GAtResultFunc func, |
| gpointer user_data, GDestroyNotify notify) |
| { |
| if (listing == NULL) |
| return 0; |
| |
| return at_chat_send_common(chat->parent, chat->group, |
| cmd, prefix_list, 0, |
| listing, func, user_data, notify); |
| } |
| |
| guint g_at_chat_send_pdu_listing(GAtChat *chat, const char *cmd, |
| const char **prefix_list, |
| GAtNotifyFunc listing, GAtResultFunc func, |
| gpointer user_data, GDestroyNotify notify) |
| { |
| if (listing == NULL) |
| return 0; |
| |
| return at_chat_send_common(chat->parent, chat->group, |
| cmd, prefix_list, |
| COMMAND_FLAG_EXPECT_PDU, |
| listing, func, user_data, notify); |
| } |
| |
| guint g_at_chat_send_and_expect_short_prompt(GAtChat *chat, const char *cmd, |
| const char **prefix_list, |
| GAtResultFunc func, |
| gpointer user_data, |
| GDestroyNotify notify) |
| { |
| return at_chat_send_common(chat->parent, chat->group, |
| cmd, prefix_list, |
| COMMAND_FLAG_EXPECT_SHORT_PROMPT, |
| NULL, func, user_data, notify); |
| } |
| |
| gboolean g_at_chat_cancel(GAtChat *chat, guint id) |
| { |
| /* We use id 0 for wakeup commands */ |
| if (chat == NULL || id == 0) |
| return FALSE; |
| |
| return at_chat_cancel(chat->parent, chat->group, id); |
| } |
| |
| gboolean g_at_chat_cancel_all(GAtChat *chat) |
| { |
| if (chat == NULL) |
| return FALSE; |
| |
| return at_chat_cancel_group(chat->parent, chat->group); |
| } |
| |
| gpointer g_at_chat_get_userdata(GAtChat *chat, guint id) |
| { |
| if (chat == NULL) |
| return NULL; |
| |
| return at_chat_get_userdata(chat->parent, chat->group, id); |
| } |
| |
| guint g_at_chat_register(GAtChat *chat, const char *prefix, |
| GAtNotifyFunc func, gboolean expect_pdu, |
| gpointer user_data, |
| GDestroyNotify destroy_notify) |
| { |
| if (chat == NULL) |
| return 0; |
| |
| return at_chat_register(chat->parent, chat->group, prefix, |
| func, expect_pdu, user_data, destroy_notify); |
| } |
| |
| gboolean g_at_chat_unregister(GAtChat *chat, guint id) |
| { |
| if (chat == NULL) |
| return FALSE; |
| |
| return at_chat_unregister(chat->parent, chat->parent->in_notify, |
| chat->group, id); |
| } |
| |
| gboolean g_at_chat_unregister_all(GAtChat *chat) |
| { |
| if (chat == NULL) |
| return FALSE; |
| |
| return at_chat_unregister_all(chat->parent, |
| chat->parent->in_notify, |
| node_compare_by_group, |
| GUINT_TO_POINTER(chat->group)); |
| } |