Desktop
/* $Id: hayes.c,v 1.13 2011/08/22 01:36:08 khorben Exp $ */
							/* Copyright (c) 2011 Pierre Pronchery <khorben@defora.org> */
							/* This file is part of DeforaOS Desktop Phone */
							/* This program is free software: you can redistribute it and/or modify
							 * it under the terms of the GNU General Public License as published by
							 * the Free Software Foundation, version 3 of the License.
							 *
							 * 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, see <http://www.gnu.org/licenses/>. */
							#include <sys/stat.h>
							#include <fcntl.h>
							#include <unistd.h>
							#include <stdlib.h>
							#include <stdio.h>
							#include <string.h>
							#include <time.h>
							#include <ctype.h>
							#include <termios.h>
							#include <errno.h>
							#include <glib.h>
							#include <System.h>
							#include <Phone/modem.h>
							#include "hayes.h"
							#define max(a, b) ((a) > (b) ? (a) : (b))
							#define min(a, b) ((a) < (b) ? (a) : (b))
							/* Hayes */
							/* private */
							/* types */
							typedef enum _HayesMode
							{
								HAYES_MODE_INIT = 0,
								HAYES_MODE_COMMAND,
								HAYES_MODE_DATA
							} HayesMode;
							typedef struct _Hayes
							{
								unsigned int retry;
								unsigned int quirks;
								/* modem */
								guint source;
								guint timeout;
								GIOChannel * channel;
								char * rd_buf;
								size_t rd_buf_cnt;
								guint rd_source;
								char * wr_buf;
								size_t wr_buf_cnt;
								guint wr_source;
								GIOChannel * rd_ppp_channel;
								guint rd_ppp_source;
								GIOChannel * wr_ppp_channel;
								guint wr_ppp_source;
								/* queue */
								HayesMode mode;
								GSList * queue;
								/* internal */
								ModemEvent events[MODEM_EVENT_TYPE_COUNT];
								char * authentication_name;
								char * call_number;
								char * contact_name;
								char * contact_number;
								char * gprs_username;
								char * gprs_password;
								char * message_number;
								char * model_name;
								char * model_vendor;
								char * model_version;
								char * registration_media;
								char * registration_operator;
							} Hayes;
							typedef enum _HayesCommandPriority
							{
								HCP_NORMAL = 0,
								HCP_IMMEDIATE
							} HayesCommandPriority;
							typedef enum _HayesCommandStatus
							{
								HCS_PENDING = 0,
								HCS_QUEUED,
								HCS_ACTIVE,
								HCS_TIMEOUT,
								HCS_ERROR,
								HCS_SUCCESS
							} HayesCommandStatus;
							typedef struct _HayesCommand HayesCommand;
							typedef HayesCommandStatus (*HayesCommandCallback)(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							struct _HayesCommand
							{
								HayesCommandPriority priority;
								HayesCommandStatus status;
								/* request */
								char * attention;
								unsigned int timeout;
								HayesCommandCallback callback;
								void * priv;
								/* answer */
								char * answer;
							};
							typedef struct _HayesRequestContactList
							{
								unsigned int from;
								unsigned int to;
							} HayesRequestContactList;
							typedef enum _HayesQuirk
							{
								HAYES_QUIRK_BATTERY_70			= 0x1,
								HAYES_QUIRK_CPIN_QUOTES			= 0x2,
								HAYES_QUIRK_CONNECTED_LINE_DISABLED	= 0x4,
								HAYES_QUIRK_WANT_SMSC_IN_PDU		= 0x8
							} HayesQuirk;
							typedef struct _HayesRequestHandler
							{
								unsigned int type;
								char const * attention;
								HayesCommandCallback callback;
							} HayesRequestHandler;
							typedef struct _HayesTriggerHandler
							{
								char const * trigger;
								void (*callback)(ModemPlugin * modem, char const * answer);
							} HayesTriggerHandler;
							/* constants */
							enum
							{
								HAYES_REQUEST_ALIVE = MODEM_REQUEST_COUNT,
								HAYES_REQUEST_BATTERY_LEVEL,
								HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_DISABLE,
								HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_ENABLE,
								HAYES_REQUEST_CONNECTED_LINE_DISABLE,
								HAYES_REQUEST_CONNECTED_LINE_ENABLE,
								HAYES_REQUEST_CONTACT_LIST,
								HAYES_REQUEST_EXTENDED_ERRORS,
								HAYES_REQUEST_EXTENDED_RING_REPORTS,
								HAYES_REQUEST_FUNCTIONAL,
								HAYES_REQUEST_FUNCTIONAL_DISABLE,
								HAYES_REQUEST_FUNCTIONAL_ENABLE,
								HAYES_REQUEST_GPRS_ATTACHED,
								HAYES_REQUEST_LOCAL_ECHO_DISABLE,
								HAYES_REQUEST_LOCAL_ECHO_ENABLE,
								HAYES_REQUEST_MESSAGE_FORMAT_PDU,
								HAYES_REQUEST_MESSAGE_UNSOLLICITED_DISABLE,
								HAYES_REQUEST_MESSAGE_UNSOLLICITED_ENABLE,
								HAYES_REQUEST_MODEL,
								HAYES_REQUEST_OPERATOR,
								HAYES_REQUEST_OPERATOR_FORMAT_SHORT,
								HAYES_REQUEST_OPERATOR_FORMAT_LONG,
								HAYES_REQUEST_OPERATOR_FORMAT_NUMERIC,
								HAYES_REQUEST_PHONE_ACTIVE,
								HAYES_REQUEST_REGISTRATION,
								HAYES_REQUEST_REGISTRATION_UNSOLLICITED_DISABLE,
								HAYES_REQUEST_REGISTRATION_UNSOLLICITED_ENABLE,
								HAYES_REQUEST_SIGNAL_LEVEL,
								HAYES_REQUEST_SIM_PIN_VALID,
								HAYES_REQUEST_SUPPLEMENTARY_SERVICE_DATA_CANCEL,
								HAYES_REQUEST_SUPPLEMENTARY_SERVICE_DATA_ENABLE,
								HAYES_REQUEST_SUPPLEMENTARY_SERVICE_DATA_DISABLE,
								HAYES_REQUEST_VENDOR,
								HAYES_REQUEST_VERBOSE_DISABLE,
								HAYES_REQUEST_VERBOSE_ENABLE,
								HAYES_REQUEST_VERSION
							};
							/* prototypes */
							/* modem */
							static int _hayes_init(ModemPlugin * modem);
							static int _hayes_destroy(ModemPlugin * modem);
							static int _hayes_start(ModemPlugin * modem, unsigned int retry);
							static int _hayes_stop(ModemPlugin * modem);
							static int _hayes_request(ModemPlugin * modem, ModemRequest * request);
							static int _hayes_trigger(ModemPlugin * modem, ModemEventType event);
							/* accessors */
							static void _hayes_set_mode(ModemPlugin * modem, HayesMode mode);
							/* useful */
							/* messages */
							static char * _hayes_message_to_pdu(ModemPlugin * modem, char const * number,
									ModemMessageEncoding encoding, size_t length,
									char const * content);
							/* conversions */
							static unsigned char _hayes_convert_char_to_iso(unsigned char c);
							static char * _hayes_convert_number_to_address(char const * number);
							/* parser */
							static int _hayes_parse(ModemPlugin * modem);
							static int _hayes_parse_trigger(ModemPlugin * modem, char const * answer,
									HayesCommand * command);
							/* queue */
							static int _hayes_queue_command(ModemPlugin * modem, HayesCommand * command);
							static int _hayes_queue_command_full(ModemPlugin * modem,
									char const * attention, HayesCommandCallback callback);
							static void _hayes_queue_flush(ModemPlugin * modem);
							static int _hayes_queue_pop(ModemPlugin * modem);
							static int _hayes_queue_push(ModemPlugin * modem);
							static int _hayes_reset(ModemPlugin * modem);
							/* commands */
							static HayesCommand * _hayes_command_new(char const * attention);
							static void _hayes_command_delete(HayesCommand * command);
							static char const * _hayes_command_get_answer(HayesCommand * command);
							static char const * _hayes_command_get_attention(HayesCommand * command);
							#if 0 /* XXX no longer being used */
							static char * _hayes_command_get_line(HayesCommand * command,
									char const * prefix);
							#endif
							static HayesCommandStatus _hayes_command_get_status(HayesCommand * command);
							static unsigned int _hayes_command_get_timeout(HayesCommand * command);
							static void _hayes_command_set_callback(HayesCommand * command,
									HayesCommandCallback callback, void * priv);
							static void _hayes_command_set_priority(HayesCommand * command,
									HayesCommandPriority priority);
							static void _hayes_command_set_status(HayesCommand * command,
									HayesCommandStatus status);
							static void _hayes_command_set_timeout(HayesCommand * command,
									unsigned int timeout);
							static int _hayes_command_answer_append(HayesCommand * command,
									char const * answer);
							static HayesCommandStatus _hayes_command_callback(HayesCommand * command);
							/* callbacks */
							static gboolean _on_reset(gpointer data);
							static gboolean _on_timeout(gpointer data);
							static gboolean _on_watch_can_read(GIOChannel * source, GIOCondition condition,
									gpointer data);
							static gboolean _on_watch_can_read_ppp(GIOChannel * source,
									GIOCondition condition, gpointer data);
							static gboolean _on_watch_can_write(GIOChannel * source, GIOCondition condition,
									gpointer data);
							static gboolean _on_watch_can_write_ppp(GIOChannel * source,
									GIOCondition condition, gpointer data);
							static HayesCommandStatus _on_request_authenticate(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_battery_level(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_call(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_call_incoming(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_call_outgoing(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_call_status(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_contact_delete(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_contact_list(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_functional(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_functional_enable(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_generic(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_message(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_message_delete(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_message_list(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_message_send(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_model(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_sim_pin_valid(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static HayesCommandStatus _on_request_unsupported(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static void _on_trigger_call_error(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cbc(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cfun(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cgatt(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cgmi(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cgmm(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cgmr(ModemPlugin * modem, char const * answer);
							static void _on_trigger_clip(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cme_error(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cmgl(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cmgr(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cmgs(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cms_error(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cmti(ModemPlugin * modem, char const * answer);
							static void _on_trigger_connect(ModemPlugin * modem, char const * answer);
							static void _on_trigger_colp(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cops(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cpas(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cpbr(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cpin(ModemPlugin * modem, char const * answer);
							static void _on_trigger_creg(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cring(ModemPlugin * modem, char const * answer);
							static void _on_trigger_csq(ModemPlugin * modem, char const * answer);
							static void _on_trigger_cusd(ModemPlugin * modem, char const * answer);
							/* helpers */
							static int _is_figure(int c);
							static int _is_number(char const * number);
							/* variables */
							typedef enum _HayesConfig
							{
								HAYES_CONFIG_BAUDRATE = 0,
								HAYES_CONFIG_HWFLOW,
								HAYES_CONFIG_DEVICE
							} HayesConfig;
							#define HAYES_CONFIG_LAST HAYES_CONFIG_DEVICE
							#define HAYES_CONFIG_COUNT (HAYES_CONFIG_LAST + 1)
							static ModemConfig _hayes_config[HAYES_CONFIG_COUNT + 1] =
							{
								{ "baudrate",	"Baudrate",		MCT_UINT32,	(void*)115200 },
								{ "hwflow",	"Hardware flow control",MCT_BOOLEAN,	(void*)1      },
								{ "device",	"Modem device",		MCT_FILENAME,	NULL	      },
								{ NULL,		NULL,			MCT_NONE,	NULL	      }
							};
							static struct
							{
								unsigned char gsm;
								unsigned char iso;
							} _hayes_gsm_iso[] =
							{
								{ '\0', '@'	},
								{ 0x01, 163	}, /* £ */
								{ 0x02, '$'	},
								{ 0x03, 165	}, /* ¥ */
								{ 0x04, 232	}, /* è */
								{ 0x05, 233	}, /* é */
								{ 0x06, 249	}, /* ù */
								{ 0x07, 236	}, /* ì */
								{ 0x08, 242	}, /* ò */
								{ 0x09, 199	}, /* Ç */
								{ 0x0b, 216	}, /* Ø */
								{ 0x0c, 248	}, /* ø */
								{ 0x0e, 197	}, /* Å */
								{ 0x0f, 229	}, /* å */
								{ 0x10, ' '	}, /* XXX delta */
								{ 0x11, '_'	},
								{ 0x12, ' '	}, /* XXX phi */
								{ 0x13, ' '	}, /* XXX gamma */
								{ 0x14, ' '	}, /* XXX lambda */
								{ 0x15, ' '	}, /* XXX omega */
								{ 0x16, ' '	}, /* XXX pi */
								{ 0x17, ' '	}, /* XXX psi */
								{ 0x18, ' '	}, /* XXX sigma */
								{ 0x19, ' '	}, /* XXX theta */
								{ 0x1a, ' '	}, /* XXX xi */
								{ 0x1b, ' '	}, /* FIXME escape */
								{ 0x1c, 198	},
								{ 0x1d, 230	},
								{ 0x1e, 223	},
								{ 0x1f, 201	},
								{ 0x24, 164	}, /* $ */
								{ 0x40, 161	}, /* @ */
								{ 0x5b, 196	}, /* [ */
								{ 0x5c, 214	}, /* \ */
								{ 0x5d, 209	}, /* ] */
								{ 0x5e, 220	}, /* ^ */
								{ 0x5f, 167	}, /* _ */
								{ 0x60, 191	}, /* ` */
								{ 0x7b, 228	}, /* { */
								{ 0x7c, 246	}, /* | */
								{ 0x7d, 241	}, /* } */
								{ 0x7e, 252	}, /* ~ */
								{ 0x7f, 224	}
							};
							static const struct
							{
								char const * model;
								unsigned int quirks;
							} _hayes_quirks[] =
							{
								{ "\"Neo1973 Embedded GSM Modem\"",
									HAYES_QUIRK_CPIN_QUOTES | HAYES_QUIRK_WANT_SMSC_IN_PDU
										| HAYES_QUIRK_CONNECTED_LINE_DISABLED		},
								{ "\"Neo1973 GTA01/GTA02 Embedded GSM Modem\"",
									HAYES_QUIRK_CPIN_QUOTES | HAYES_QUIRK_WANT_SMSC_IN_PDU
										| HAYES_QUIRK_CONNECTED_LINE_DISABLED		},
								{ "\"Neo1973 GTA02 Embedded GSM Modem\"",
									HAYES_QUIRK_CPIN_QUOTES | HAYES_QUIRK_WANT_SMSC_IN_PDU
										| HAYES_QUIRK_CONNECTED_LINE_DISABLED		},
								{ "Nokia N900",
									HAYES_QUIRK_CPIN_QUOTES | HAYES_QUIRK_BATTERY_70	},
								{ NULL,			0					}
							};
							static HayesRequestHandler _hayes_request_handlers[] =
							{
								{ HAYES_REQUEST_ALIVE,				"AT",
									_on_request_generic },
								{ HAYES_REQUEST_BATTERY_LEVEL,			"AT+CBC",
									_on_request_battery_level },
								{ HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_DISABLE,"AT+CCWA=1",
									_on_request_generic },
								{ HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_ENABLE,"AT+CCWA=1",
									_on_request_generic },
								{ HAYES_REQUEST_CONNECTED_LINE_DISABLE,		"AT+COLP=0",
									_on_request_generic },
								{ HAYES_REQUEST_CONNECTED_LINE_ENABLE,		"AT+COLP=1",
									_on_request_generic },
								{ HAYES_REQUEST_CONTACT_LIST,			NULL,
									_on_request_generic },
								{ HAYES_REQUEST_EXTENDED_ERRORS,		"AT+CMEE=1",
									_on_request_generic },
								{ HAYES_REQUEST_EXTENDED_RING_REPORTS,		"AT+CRC=1",
									_on_request_generic },
								{ HAYES_REQUEST_FUNCTIONAL,			"AT+CFUN?",
									_on_request_functional },
								{ HAYES_REQUEST_FUNCTIONAL_DISABLE,		"AT+CFUN=0",
									_on_request_generic },
								{ HAYES_REQUEST_FUNCTIONAL_ENABLE,		"AT+CFUN=1",
									_on_request_functional_enable },
								{ HAYES_REQUEST_GPRS_ATTACHED,			"AT+CGATT?",
									_on_request_generic },
								{ HAYES_REQUEST_LOCAL_ECHO_DISABLE,		"ATE0",
									_on_request_generic },
								{ HAYES_REQUEST_LOCAL_ECHO_ENABLE,		"ATE1",
									_on_request_generic },
								{ HAYES_REQUEST_MESSAGE_FORMAT_PDU,		"AT+CMGF=0",
									_on_request_generic },
								{ HAYES_REQUEST_MESSAGE_UNSOLLICITED_DISABLE,	"AT+CNMI=0",
									_on_request_generic },
								{ HAYES_REQUEST_MESSAGE_UNSOLLICITED_ENABLE,	"AT+CNMI=1",
									_on_request_generic }, /* XXX report error? */
								{ HAYES_REQUEST_MODEL,				"AT+CGMM",
									_on_request_model },
								{ HAYES_REQUEST_OPERATOR,			"AT+COPS?",
									_on_request_generic },
								{ HAYES_REQUEST_OPERATOR_FORMAT_LONG,		"AT+COPS=3,0",
									_on_request_generic },
								{ HAYES_REQUEST_OPERATOR_FORMAT_NUMERIC,	"AT+COPS=3,2",
									_on_request_generic },
								{ HAYES_REQUEST_OPERATOR_FORMAT_SHORT,		"AT+COPS=3,1",
									_on_request_generic },
								{ HAYES_REQUEST_PHONE_ACTIVE,			"AT+CPAS",
									_on_request_call },
								{ HAYES_REQUEST_REGISTRATION,			"AT+CREG?",
									_on_request_generic },
								{ HAYES_REQUEST_REGISTRATION_UNSOLLICITED_DISABLE,"AT+CREG=0",
									_on_request_generic },
								{ HAYES_REQUEST_REGISTRATION_UNSOLLICITED_ENABLE,"AT+CREG=2",
									_on_request_generic },
								{ HAYES_REQUEST_SIGNAL_LEVEL,			"AT+CSQ",
									_on_request_generic },
								{ HAYES_REQUEST_SIM_PIN_VALID,			"AT+CPIN?",
									_on_request_sim_pin_valid },
								{ HAYES_REQUEST_SUPPLEMENTARY_SERVICE_DATA_CANCEL,"AT+CUSD=2",
									_on_request_generic },
								{ HAYES_REQUEST_SUPPLEMENTARY_SERVICE_DATA_DISABLE,"AT+CUSD=0",
									_on_request_generic },
								{ HAYES_REQUEST_SUPPLEMENTARY_SERVICE_DATA_ENABLE,"AT+CUSD=1",
									_on_request_generic },
								{ HAYES_REQUEST_VENDOR,				"AT+CGMI",
									_on_request_model },
								{ HAYES_REQUEST_VERBOSE_DISABLE,		"ATV0",
									_on_request_generic },
								{ HAYES_REQUEST_VERBOSE_ENABLE,			"ATV1",
									_on_request_generic },
								{ HAYES_REQUEST_VERSION,			"AT+CGMR",
									_on_request_model },
								{ MODEM_REQUEST_AUTHENTICATE,			NULL,
									_on_request_authenticate },
								{ MODEM_REQUEST_CALL,				NULL,
									_on_request_call_outgoing },
								{ MODEM_REQUEST_CALL_ANSWER,			"ATA",
									_on_request_call_incoming },
								{ MODEM_REQUEST_CALL_HANGUP,			NULL,
									_on_request_call_status },
								{ MODEM_REQUEST_CALL_PRESENTATION,		NULL,
									_on_request_generic },
								{ MODEM_REQUEST_CONTACT_DELETE,			NULL,
									_on_request_contact_delete },
								{ MODEM_REQUEST_CONTACT_LIST,			"AT+CPBR=?",
									_on_request_contact_list },
								{ MODEM_REQUEST_MESSAGE,			NULL,
									_on_request_message },
								{ MODEM_REQUEST_MESSAGE_DELETE,			NULL,
									_on_request_message_delete },
								{ MODEM_REQUEST_MESSAGE_LIST,			"AT+CMGL=4",
									_on_request_message_list },
								{ MODEM_REQUEST_MESSAGE_SEND,			NULL,
									_on_request_message_send },
								{ MODEM_REQUEST_REGISTRATION,			NULL,
									_on_request_generic }, /* FIXME really track */
								{ MODEM_REQUEST_UNSUPPORTED,			NULL,
									_on_request_unsupported }
							};
							static HayesTriggerHandler _hayes_trigger_handlers[] =
							{
								{ "+CBC",	_on_trigger_cbc		},
								{ "+CFUN",	_on_trigger_cfun	},
								{ "+CGATT",	_on_trigger_cgatt	},
								{ "+CGMI",	_on_trigger_cgmi	},
								{ "+CGMM",	_on_trigger_cgmm	},
								{ "+CGMR",	_on_trigger_cgmr	},
								{ "+CLIP",	_on_trigger_clip	},
								{ "+CME ERROR",	_on_trigger_cme_error	},
								{ "+CMGL",	_on_trigger_cmgl	},
								{ "+CMGR",	_on_trigger_cmgr	},
								{ "+CMGS",	_on_trigger_cmgs	},
								{ "+CMS ERROR",	_on_trigger_cms_error	},
								{ "+CMTI",	_on_trigger_cmti	},
								{ "+COLP",	_on_trigger_colp	},
								{ "+COPS",	_on_trigger_cops	},
								{ "+CPAS",	_on_trigger_cpas	},
								{ "+CPBR",	_on_trigger_cpbr	},
								{ "+CPIN",	_on_trigger_cpin	},
								{ "+CREG",	_on_trigger_creg	},
								{ "+CRING",	_on_trigger_cring	},
								{ "+CSQ",	_on_trigger_csq		},
								{ "+CUSD",	_on_trigger_cusd	},
								{ "BUSY",	_on_trigger_call_error	},
								{ "CONNECT",	_on_trigger_connect	},
								{ "NO CARRIER",	_on_trigger_call_error	},
								{ "NO DIALTONE",_on_trigger_call_error	},
								{ "RING",	_on_trigger_cring	}
							};
							/* public */
							/* variables */
							ModemPlugin plugin =
							{
								NULL,
								"Hayes",
								NULL,
								_hayes_config,
								_hayes_init,
								_hayes_destroy,
								_hayes_start,
								_hayes_stop,
								_hayes_request,
								_hayes_trigger,
								NULL
							};
							/* private */
							/* functions */
							static int _hayes_init(ModemPlugin * modem)
							{
								Hayes * hayes;
								size_t i;
								if((hayes = object_new(sizeof(*hayes))) == NULL)
									return -1;
								memset(hayes, 0, sizeof(*hayes));
								modem->priv = hayes;
								hayes->mode = HAYES_MODE_INIT;
								for(i = 0; i < sizeof(hayes->events) / sizeof(*hayes->events); i++)
								{
									hayes->events[i].type = i;
									if(i == MODEM_EVENT_TYPE_REGISTRATION)
										hayes->events[i].registration.signal = 0.0 / 0.0;
								}
								return 0;
							}
							/* hayes_destroy */
							static int _hayes_destroy(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								_hayes_stop(modem);
								string_delete(hayes->authentication_name);
								string_delete(hayes->call_number);
								string_delete(hayes->contact_name);
								string_delete(hayes->contact_number);
								string_delete(hayes->gprs_username);
								string_delete(hayes->gprs_password);
								string_delete(hayes->message_number);
								string_delete(hayes->model_name);
								string_delete(hayes->model_vendor);
								string_delete(hayes->model_version);
								string_delete(hayes->registration_media);
								string_delete(hayes->registration_operator);
								object_delete(hayes);
								return 0;
							}
							/* hayes_request */
							static char * _request_attention(ModemPlugin * modem, ModemRequest * request);
							static char * _request_attention_apn(char const * protocol, char const * apn);
							static char * _request_attention_call(ModemPlugin * modem,
									ModemRequest * request);
							static char * _request_attention_call_hangup(ModemPlugin * modem);
							static char * _request_attention_contact_delete(ModemPlugin * modem,
									unsigned int id);
							static char * _request_attention_contact_list(ModemRequest * request);
							static char * _request_attention_gprs(ModemPlugin * modem,
									char const * username, char const * password);
							static char * _request_attention_message(ModemPlugin * modem, unsigned int id);
							static char * _request_attention_message_delete(ModemPlugin * modem,
									unsigned int id);
							static char * _request_attention_message_send(ModemPlugin * modem,
									char const * number, ModemMessageEncoding encoding,
									size_t length, char const * content);
							static char * _request_attention_sim_pin(ModemPlugin * modem,
									char const * password);
							static char * _request_attention_sim_puk(ModemPlugin * modem,
									char const * password);
							static char * _request_registration(ModemPlugin * modem,
									ModemRequest * request);
							static char * _request_unsupported(ModemPlugin * modem, ModemRequest * request);
							static int _hayes_request(ModemPlugin * modem, ModemRequest * request)
							{
								int ret;
								Hayes * hayes = modem->priv;
								size_t i;
								size_t count = sizeof(_hayes_request_handlers)
									/ sizeof(*_hayes_request_handlers);
								char const * attention;
								char * p = NULL;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s(%u)\n", __func__, (request != NULL)
										? request->type : (unsigned)-1);
							#endif
								if(request == NULL)
									return -1;
								if(hayes->quirks & HAYES_QUIRK_CONNECTED_LINE_DISABLED
										&& request->type == HAYES_REQUEST_CONNECTED_LINE_ENABLE)
									request->type = HAYES_REQUEST_CONNECTED_LINE_DISABLE;
								for(i = 0; i < count; i++)
									if(_hayes_request_handlers[i].type == request->type)
										break;
								if(i == count)
							#ifdef DEBUG
									return -modem->helper->error(modem->helper->modem,
											"Unable to handle request", 1);
							#else
									return -1;
							#endif
								if((attention = _hayes_request_handlers[i].attention) == NULL)
								{
									if((p = _request_attention(modem, request)) == NULL)
										return 0; /* XXX errors should not be ignored */
									attention = p;
								}
								ret = _hayes_queue_command_full(modem, attention,
										_hayes_request_handlers[i].callback);
								free(p);
								return ret;
							}
							static char * _request_attention(ModemPlugin * modem, ModemRequest * request)
							{
								char buf[32];
								switch(request->type)
								{
									case HAYES_REQUEST_CONTACT_LIST:
										return _request_attention_contact_list(request);
									case MODEM_REQUEST_AUTHENTICATE:
										if(strcmp(request->authenticate.name, "APN") == 0)
											return _request_attention_apn(
													request->authenticate.username,
													request->authenticate.password);
										if(strcmp(request->authenticate.name, "GPRS") == 0)
											return _request_attention_gprs(modem,
													request->authenticate.username,
													request->authenticate.password);
										if(strcmp(request->authenticate.name, "SIM PIN") == 0)
											return _request_attention_sim_pin(modem,
													request->authenticate.password);
										if(strcmp(request->authenticate.name, "SIM PUK") == 0)
											return _request_attention_sim_puk(modem,
													request->authenticate.password);
										break;
									case MODEM_REQUEST_CALL:
										return _request_attention_call(modem, request);
									case MODEM_REQUEST_CALL_HANGUP:
										return _request_attention_call_hangup(modem);
									case MODEM_REQUEST_CALL_PRESENTATION:
										snprintf(buf, sizeof(buf), "%s%u", "AT+CLIP=",
												request->call_presentation.enabled
												? 1 : 0);
										return strdup(buf);
									case MODEM_REQUEST_CONTACT_DELETE:
										return _request_attention_contact_delete(modem,
												request->contact_delete.id);
									case MODEM_REQUEST_MESSAGE:
										return _request_attention_message(modem,
												request->message.id);
									case MODEM_REQUEST_MESSAGE_DELETE:
										return _request_attention_message_delete(modem,
												request->message_delete.id);
									case MODEM_REQUEST_MESSAGE_SEND:
										return _request_attention_message_send(modem,
												request->message_send.number,
												request->message_send.encoding,
												request->message_send.length,
												request->message_send.content);
									case MODEM_REQUEST_REGISTRATION:
										return _request_registration(modem, request);
									case MODEM_REQUEST_UNSUPPORTED:
										return _request_unsupported(modem, request);
									default:
										break;
								}
								return NULL;
							}
							static char * _request_attention_apn(char const * protocol, char const * apn)
							{
								char * ret;
								const char cmd[] = "AT+CGDCONT=1,";
								size_t len;
								if(protocol == NULL || apn == NULL)
									return NULL;
								len = sizeof(cmd) + strlen(protocol) + 2 + strlen(apn) + 3;
								if((ret = malloc(len)) == NULL)
									return NULL;
								snprintf(ret, len, "%s\"%s\",\"%s\"", cmd, protocol, apn);
								return ret;
							}
							static char * _request_attention_call(ModemPlugin * modem,
									ModemRequest * request)
							{
								char * ret;
								Hayes * hayes = modem->priv;
								char const * number = request->call.number;
								ModemEvent * event;
								const char cmd[] = "ATD";
								const char anonymous[] = "I";
								const char voice[] = ";";
								size_t len;
								if(request->call.number == NULL)
									request->call.number = "";
								if(request->call.number[0] == '\0')
									number = "L";
								else if(!_is_number(request->call.number))
									return NULL;
								event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								free(hayes->call_number);
								if(request->call.call_type == MODEM_CALL_TYPE_DATA)
									hayes->call_number = NULL;
								else if((hayes->call_number = strdup(request->call.number)) == NULL)
									return NULL;
								event->call.number = hayes->call_number;
								len = sizeof(cmd) + strlen(number) + sizeof(anonymous) + sizeof(voice);
								if((ret = malloc(len)) == NULL)
									return NULL;
								snprintf(ret, len, "%s%s%s%s", "ATD", number,
										(request->call.anonymous) ? anonymous : "",
										(request->call.call_type == MODEM_CALL_TYPE_VOICE)
										? voice : "");
								return ret;
							}
							static char * _request_attention_call_hangup(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CONNECTION];
								/* FIXME check that this works on all phones, including:
								 * - while calling (still ringing) => simply inject "\r\n"?
								 * - while ringing (incoming) */
								if(hayes->mode == HAYES_MODE_DATA)
								{
									event->connection.connected = 0;
									event->connection.in = 0;
									event->connection.out = 0;
									modem->helper->event(modem->helper->modem, event);
									_hayes_set_mode(modem, HAYES_MODE_INIT);
									return NULL;
								}
								return strdup("ATH");
							}
							static char * _request_attention_contact_delete(ModemPlugin * modem,
									unsigned int id)
							{
								Hayes * hayes = modem->priv;
								char const cmd[] = "AT+CPBW=";
								char buf[32];
								/* FIXME store in the command itself */
								hayes->events[MODEM_EVENT_TYPE_CONTACT_DELETED].contact_deleted.id = id;
								snprintf(buf, sizeof(buf), "%s%u%s", cmd, id, ",");
								return strdup(buf);
							}
							static char * _request_attention_contact_list(ModemRequest * request)
							{
								HayesRequestContactList * list = request->plugin.data;
								const char cmd[] = "AT+CPBR=";
								char buf[32];
								if(list->to < list->from)
									list->to = list->from;
								snprintf(buf, sizeof(buf), "%s%u,%u", cmd, list->from, list->to);
								return strdup(buf);
							}
							static char * _request_attention_gprs(ModemPlugin * modem,
									char const * username, char const * password)
							{
								Hayes * hayes = modem->priv;
								free(hayes->gprs_username);
								hayes->gprs_username = (username != NULL) ? strdup(username) : NULL;
								free(hayes->gprs_password);
								hayes->gprs_password = (password != NULL) ? strdup(password) : NULL;
								return NULL; /* we don't need to issue any command */
							}
							static char * _request_attention_message(ModemPlugin * modem, unsigned int id)
							{
								char const cmd[] = "AT+CMGR=";
								char buf[32];
								/* FIXME force the message format to be in PDU mode? */
								snprintf(buf, sizeof(buf), "%s%u", cmd, id);
								return strdup(buf);
							}
							static char * _request_attention_message_delete(ModemPlugin * modem,
									unsigned int id)
							{
								Hayes * hayes = modem->priv;
								char const cmd[] = "AT+CMGD=";
								char buf[32];
								/* FIXME store in the command itself */
								hayes->events[MODEM_EVENT_TYPE_MESSAGE_DELETED].message_deleted.id = id;
								snprintf(buf, sizeof(buf), "%s%u", cmd, id);
								return strdup(buf);
							}
							static char * _request_attention_message_send(ModemPlugin * modem,
									char const * number, ModemMessageEncoding encoding,
									size_t length, char const * content)
							{
								Hayes * hayes = modem->priv;
								char * ret;
								char const cmd[] = "AT+CMGS=";
								ModemRequest request;
								char * pdu;
								size_t pdulen;
								size_t len;
								memset(&request, 0, sizeof(request));
								request.type = HAYES_REQUEST_MESSAGE_FORMAT_PDU;
								if(_hayes_request(modem, &request) != 0)
									return NULL;
								if((pdu = _hayes_message_to_pdu(modem, number, encoding, length,
												content)) == NULL)
									return NULL;
								pdulen = strlen(pdu);
								len = sizeof(cmd) + 10 + pdulen + 1;
								ret = malloc(len);
								if(hayes->quirks & HAYES_QUIRK_WANT_SMSC_IN_PDU)
									pdulen-=2;
								/* FIXME really issue using two separate commands */
								if(ret != NULL)
									snprintf(ret, len, "%s%lu\r\n%s", cmd, (pdulen - 1) / 2, pdu);
								free(pdu);
								return ret;
							}
							static char * _request_attention_sim_pin(ModemPlugin * modem,
									char const * password)
							{
								char * ret;
								Hayes * hayes = modem->priv;
								const char cmd[] = "AT+CPIN=";
								size_t len;
								char const * format;
								if(password == NULL)
									return NULL;
								len = sizeof(cmd) + strlen(password) + 2;
								if((ret = malloc(len)) == NULL)
									return NULL;
								format = (hayes->quirks & HAYES_QUIRK_CPIN_QUOTES) ? "%s\"%s\""
									: "%s%s";
								snprintf(ret, len, format, cmd, password);
								return ret;
							}
							static char * _request_attention_sim_puk(ModemPlugin * modem,
									char const * password)
							{
								char * ret;
								Hayes * hayes = modem->priv;
								const char cmd[] = "AT+CPIN=";
								size_t len;
								char const * format;
								if(password == NULL)
									return NULL;
								len = sizeof(cmd) + strlen(password) + 3;
								if((ret = malloc(len)) == NULL)
									return NULL;
								format = (hayes->quirks & HAYES_QUIRK_CPIN_QUOTES) ? "%s\"%s\","
									: "%s%s,";
								snprintf(ret, len, format, cmd, password);
								return ret;
							}
							static char * _request_registration(ModemPlugin * modem, ModemRequest * request)
							{
								char const cops[] = "AT+COPS=";
								size_t len = sizeof(cops) + 5;
								char * p;
								switch(request->registration.mode)
								{
									case MODEM_REGISTRATION_MODE_AUTOMATIC:
										return strdup("AT+COPS=0");
									case MODEM_REGISTRATION_MODE_DISABLED:
										return strdup("AT+COPS=2");
									case MODEM_REGISTRATION_MODE_MANUAL:
										if(request->registration._operator == NULL)
											return NULL;
										len += strlen(request->registration._operator);
										if((p = malloc(len)) == NULL)
											return NULL;
										snprintf(p, len, "%s=1,0,%s", cops,
												request->registration._operator);
										return p;
									case MODEM_REGISTRATION_MODE_UNKNOWN:
										break;
								}
								return NULL;
							}
							static char * _request_unsupported(ModemPlugin * modem, ModemRequest * request)
							{
								HayesRequest * hrequest = request->unsupported.request;
								if(strcmp(request->unsupported.modem, plugin.name) != 0)
									return NULL;
								if(request->unsupported.size != sizeof(*hrequest))
									return NULL;
								switch(request->unsupported.request_type)
								{
									case HAYES_REQUEST_COMMAND_QUEUE:
										return strdup(hrequest->command_queue.command);
									default:
										return NULL;
								}
							}
							/* hayes_start */
							static int _hayes_start(ModemPlugin * modem, unsigned int retry)
							{
								Hayes * hayes = modem->priv;
								hayes->retry = retry;
								hayes->source = g_idle_add(_on_reset, modem);
								return 0;
							}
							/* hayes_stop */
							static void _stop_channel(GIOChannel * channel);
							static int _hayes_stop(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event;
								/* close everything opened */
								_hayes_queue_flush(modem);
								_stop_channel(hayes->channel);
								hayes->channel = NULL;
								_stop_channel(hayes->rd_ppp_channel);
								hayes->rd_ppp_channel = NULL;
								_stop_channel(hayes->wr_ppp_channel);
								hayes->wr_ppp_channel = NULL;
								/* report disconnection if already connected */
								event = &hayes->events[MODEM_EVENT_TYPE_CONNECTION];
								if(event->connection.connected)
								{
									event->connection.connected = 0;
									event->connection.in = 0;
									event->connection.out = 0;
									modem->helper->event(modem->helper->modem, event);
								}
								/* remove registration data */
								string_delete(hayes->registration_media);
								hayes->registration_media = NULL;
								event->registration.media = NULL;
								string_delete(hayes->registration_operator);
								hayes->registration_operator = NULL;
								event->registration._operator = NULL;
								event->registration.signal = 0.0 / 0.0;
								event->registration.roaming = 0;
								/* report as being offline */
								event = &hayes->events[MODEM_EVENT_TYPE_STATUS];
								event->status.online = 0;
								modem->helper->event(modem->helper->modem, event);
								/* FIXME some more? */
								return 0;
							}
							static void _stop_channel(GIOChannel * channel)
							{
								GError * error = NULL;
								if(channel == NULL)
									return;
								/* XXX should the file descriptor also be closed? */
								if(g_io_channel_shutdown(channel, TRUE, &error) == G_IO_STATUS_ERROR)
									/* XXX report error */
									g_error_free(error);
								g_io_channel_unref(channel);
							}
							/* hayes_trigger */
							static int _hayes_trigger(ModemPlugin * modem, ModemEventType event)
							{
								int ret = 0;
								Hayes * hayes = modem->priv;
								ModemRequest request;
								ModemEvent * e;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s(%u)\n", __func__, event);
							#endif
								memset(&request, 0, sizeof(request));
								switch(event)
								{
									case MODEM_EVENT_TYPE_AUTHENTICATION:
										request.type = HAYES_REQUEST_SIM_PIN_VALID;
										return _hayes_request(modem, &request);
									case MODEM_EVENT_TYPE_BATTERY_LEVEL:
										request.type = HAYES_REQUEST_BATTERY_LEVEL;
										return _hayes_request(modem, &request);
									case MODEM_EVENT_TYPE_CALL:
										request.type = HAYES_REQUEST_PHONE_ACTIVE;
										return _hayes_request(modem, &request);
									case MODEM_EVENT_TYPE_CONNECTION:
										e = &hayes->events[MODEM_EVENT_TYPE_CONNECTION];
										modem->helper->event(modem->helper->modem, e);
										break;
									case MODEM_EVENT_TYPE_CONTACT:
										request.type = MODEM_REQUEST_CONTACT_LIST;
										return _hayes_request(modem, &request);
									case MODEM_EVENT_TYPE_MODEL:
										request.type = HAYES_REQUEST_VENDOR;
										ret |= _hayes_request(modem, &request);
										request.type = HAYES_REQUEST_MODEL;
										ret |= _hayes_request(modem, &request);
										request.type = HAYES_REQUEST_VERSION;
										ret |= _hayes_request(modem, &request);
										break;
									case MODEM_EVENT_TYPE_REGISTRATION:
										request.type = HAYES_REQUEST_REGISTRATION;
										ret |= _hayes_request(modem, &request);
										break;
									case MODEM_EVENT_TYPE_STATUS:
										e = &hayes->events[MODEM_EVENT_TYPE_STATUS];
										modem->helper->event(modem->helper->modem, e);
										break;
									case MODEM_EVENT_TYPE_CONTACT_DELETED: /* do not make sense */
									case MODEM_EVENT_TYPE_ERROR:
									case MODEM_EVENT_TYPE_MESSAGE:
									case MODEM_EVENT_TYPE_MESSAGE_DELETED:
									case MODEM_EVENT_TYPE_MESSAGE_SENT:
										ret = -1;
										break;
								}
								return ret;
							}
							/* accessors */
							static void _hayes_set_mode(ModemPlugin * modem, HayesMode mode)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event;
								if(hayes->mode == mode)
									return;
								switch(hayes->mode)
								{
									case HAYES_MODE_INIT:
									case HAYES_MODE_COMMAND:
										break; /* nothing to do */
									case HAYES_MODE_DATA:
										if(hayes->rd_ppp_source != 0)
											g_source_remove(hayes->rd_ppp_source);
										hayes->rd_ppp_source = 0;
										if(hayes->wr_ppp_source != 0)
											g_source_remove(hayes->wr_ppp_source);
										hayes->rd_ppp_source = 0;
										/* reset registration media */
										event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
										free(hayes->registration_media);
										hayes->registration_media = NULL;
										event->registration.media = NULL;
										/* reset modem */
										_hayes_reset(modem);
										break;
								}
								switch(mode)
								{
									case HAYES_MODE_INIT:
									case HAYES_MODE_COMMAND:
										break; /* nothing to do */
									case HAYES_MODE_DATA:
										/* report GPRS registration */
										event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
										free(hayes->registration_media);
										hayes->registration_media = strdup("GPRS");
										event->registration.media = hayes->registration_media;
										modem->helper->event(modem->helper->modem, event);
										break;
								}
								hayes->mode = mode;
							}
							/* messages */
							/* hayes_message_to_pdu */
							static char * _text_to_data(char const * text, size_t length);
							static char * _text_to_sept(char const * text, size_t length);
							static char * _hayes_message_to_pdu(ModemPlugin * modem, char const * number,
									ModemMessageEncoding encoding, size_t length,
									char const * content)
							{
								Hayes * hayes = modem->priv;
								char * ret;
								char * addr;
								char * data;
								char * p = NULL;
								size_t len;
								char const * smsc = "";
								char const prefix[] = "1100";
								char const pid[] = "00";
								char dcs[] = "0X";
								char const vp[] = "AA";
								if(!_is_number(number))
									return NULL;
								switch(encoding)
								{
									case MODEM_MESSAGE_ENCODING_UTF8:
										/* FIXME really support UTF-8 when necessary */
										p = g_convert(content, length, "ISO-8859-1", "UTF-8",
												NULL, NULL, NULL);
										if(p == NULL)
											return NULL;
										content = p;
										length = strlen(content);
									case MODEM_MESSAGE_ENCODING_ASCII:
										dcs[1] = '0';
										data = _text_to_sept(content, length);
										break;
									case MODEM_MESSAGE_ENCODING_DATA:
										dcs[1] = '4';
										data = _text_to_data(content, length);
										break;
									default:
										return NULL;
								}
								addr = _hayes_convert_number_to_address(number);
								len = 2 + sizeof(prefix) + 2 + strlen((addr != NULL) ? addr : "")
									+ sizeof(pid) + sizeof(dcs) + sizeof(vp) + 2
									+ strlen((data != NULL) ? data : "") + 1;
								ret = malloc(len);
								if(addr != NULL && ret != NULL)
								{
									if(hayes->quirks & HAYES_QUIRK_WANT_SMSC_IN_PDU)
										smsc = "00";
									snprintf(ret, len, "%s%s%02lX%s%s%s%s%02lX%s\x1a",
											smsc, prefix, (unsigned long)strlen(number),
											addr, pid, dcs, vp, (unsigned long)length,
											data);
								}
								free(data);
								free(addr);
								free(p);
								return ret;
							}
							static char * _text_to_data(char const * text, size_t length)
							{
								char const tab[16] = "0123456789ABCDEF";
								char * buf;
								size_t i;
								if((buf = malloc((length * 2) + 1)) == NULL)
									return NULL;
								for(i = 0; i < length; i++)
								{
									buf[(i * 2) + 1] = tab[text[i] & 0x0f];
									buf[i * 2] = tab[((text[i] & 0xf0) >> 4) & 0x0f];
								}
								buf[i * 2] = '\0';
								return buf;
							}
							/* this function is heavily inspired from gsmd, (c) 2007 OpenMoko, Inc. */
							static char * _text_to_sept(char const * text, size_t length)
							{
								char const tab[16] = "0123456789ABCDEF";
								unsigned char const * t = (unsigned char const *)text;
								char * buf;
								char * p;
								size_t i;
								unsigned char ch1;
								unsigned char ch2;
								int shift = 0;
								if((buf = malloc((length * 2) + 1)) == NULL)
									return NULL;
								p = buf;
								for(i = 0; i < length; i++)
								{
									ch1 = t[i] & 0x7f;
									ch1 = (ch1 >> shift);
									ch2 = t[i + 1] & 0x7f;
									ch2 = ch2 << (7 - shift);
									ch1 = ch1 | ch2;
									*(p++) = tab[(ch1 & 0xf0) >> 4];
									*(p++) = tab[ch1 & 0x0f];
									if(++shift == 7)
									{
										shift = 0;
										i++;
									}
								}
								*p = '\0';
								return buf;
							}
							/* conversions */
							/* hayes_convert_char_to_iso */
							static unsigned char _hayes_convert_char_to_iso(unsigned char c)
							{
								size_t i;
								for(i = 0; i < sizeof(_hayes_gsm_iso) / sizeof(*_hayes_gsm_iso); i++)
									if(_hayes_gsm_iso[i].gsm == c)
										return _hayes_gsm_iso[i].iso;
								return c;
							}
							/* hayes_convert_number_to_address */
							static char * _hayes_convert_number_to_address(char const * number)
							{
								char * ret;
								size_t len;
								size_t i;
								len = 2 + strlen(number) + 2;
								if((ret = malloc(len)) == NULL)
									return NULL;
								snprintf(ret, len, "%02X", (number[0] == '+') ? 145 : 129);
								if(number[0] == '+')
									number++;
								for(i = 2; i < len; i+=2)
								{
									if(number[i - 2] == '\0')
										break;
									ret[i] = number[i - 1];
									ret[i + 1] = number[i - 2];
									if(number[i - 1] == '\0')
									{
										ret[i] = 'F';
										i+=2;
										break;
									}
								}
								ret[i] = '\0';
								return ret;
							}
							/* parser */
							/* hayes_parse */
							static int _parse_do(ModemPlugin * modem);
							static int _hayes_parse(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								int ret = 0;
								size_t i = 0;
								char * p;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s() cnt=%lu\n", __func__,
										(unsigned long)hayes->rd_buf_cnt);
							#endif
								while(i < hayes->rd_buf_cnt)
								{
									if(hayes->rd_buf[i++] != '\r' && hayes->rd_buf[i - 1] != '\n')
										continue;
									hayes->rd_buf[i - 1] = '\0';
									if(i < hayes->rd_buf_cnt && hayes->rd_buf[i] == '\n')
										i++;
									if(hayes->rd_buf[0] != '\0')
										ret |= _parse_do(modem);
									hayes->rd_buf_cnt -= i;
									memmove(hayes->rd_buf, &hayes->rd_buf[i], hayes->rd_buf_cnt);
									if((p = realloc(hayes->rd_buf, hayes->rd_buf_cnt)) != NULL)
										hayes->rd_buf = p; /* we can ignore errors... */
									else if(hayes->rd_buf_cnt == 0)
										hayes->rd_buf = NULL; /* ...except when it's not one */
									i = 0;
								}
							#if 0
								if(hayes->mode == HAYES_MODE_PDU)
									return _parse_pdu(modem);
							#endif
								return ret;
							}
							static int _parse_do(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								HayesCommand * command = (hayes->queue != NULL) ? hayes->queue->data
									: NULL;
								HayesCommandStatus status;
								if(command == NULL)
									/* this was most likely unsollicited */
									return _hayes_parse_trigger(modem, hayes->rd_buf, NULL);
								if(_hayes_command_answer_append(command, hayes->rd_buf) != 0)
									return -1;
								if((status = _hayes_command_callback(command)) == HCS_ACTIVE)
									_hayes_parse_trigger(modem, hayes->rd_buf, command);
								if((status = _hayes_command_get_status(command)) == HCS_SUCCESS
										|| status == HCS_ERROR)
								{
									_hayes_queue_pop(modem);
									_hayes_queue_push(modem);
								}
								return 0;
							}
							/* hayes_parse_trigger */
							static int _hayes_parse_trigger(ModemPlugin * modem, char const * answer,
									HayesCommand * command)
							{
								size_t i;
								size_t count = sizeof(_hayes_trigger_handlers)
									/ sizeof(*_hayes_trigger_handlers);
								size_t len;
								HayesTriggerHandler * th;
								char const * p;
								int j;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s(modem, \"%s\", command)\n", __func__,
										answer);
							#endif
								/* if the trigger is obvious return directly */
								for(i = 0; i < count; i++)
								{
									th = &_hayes_trigger_handlers[i];
									len = strlen(th->trigger);
									if(strncmp(th->trigger, answer, len) != 0)
										continue;
									if(answer[len] == ':')
									{
										if(answer[++len] == ' ') /* skip the optional space */
											len++;
									}
									else if(answer[len] != '\0')
										continue;
									th->callback(modem, &answer[len]);
									return 0;
								}
								/* if the answer has no prefix choose it from the command issued */
								if(command == NULL
										|| (p = _hayes_command_get_attention(command)) == NULL
										|| strncmp(p, "AT", 2) != 0)
									return 0;
								for(i = 0; i < count; i++)
								{
									th = &_hayes_trigger_handlers[i];
									len = strlen(th->trigger);
									if(strncmp(th->trigger, &p[2], len) != 0
											|| isalnum((j = p[2 + len])))
										continue;
									th->callback(modem, answer);
									return 0;
								}
								return 0;
							}
							/* queue */
							/* hayes_queue_command */
							static int _hayes_queue_command(ModemPlugin * modem, HayesCommand * command)
							{
								Hayes * hayes = modem->priv;
								GSList * queue;
								switch(hayes->mode)
								{
									case HAYES_MODE_INIT:
										/* FIXME should ignore commands sent at this point */
									case HAYES_MODE_COMMAND:
									case HAYES_MODE_DATA:
										queue = hayes->queue;
										hayes->queue = g_slist_append(hayes->queue, command);
										_hayes_command_set_status(command, HCS_QUEUED);
										if(queue == NULL)
											_hayes_queue_push(modem);
										break;
								}
								return 0;
							}
							/* hayes_queue_command_full */
							static int _hayes_queue_command_full(ModemPlugin * modem,
									char const * attention, HayesCommandCallback callback)
							{
								HayesCommand * command;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, attention);
							#endif
								if((command = _hayes_command_new(attention)) == NULL)
									return -modem->helper->error(modem->helper->modem, error_get(),
											1);
								_hayes_command_set_callback(command, callback, modem);
								if(_hayes_queue_command(modem, command) != 0)
								{
									_hayes_command_delete(command);
									return -1;
								}
								return 0;
							}
							/* hayes_queue_flush */
							static void _hayes_queue_flush(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								g_slist_foreach(hayes->queue, (GFunc)_hayes_command_delete, NULL);
								g_slist_free(hayes->queue);
								hayes->queue = NULL;
								free(hayes->rd_buf);
								hayes->rd_buf = NULL;
								hayes->rd_buf_cnt = 0;
								if(hayes->rd_source != 0)
									g_source_remove(hayes->rd_source);
								hayes->rd_source = 0;
								hayes->wr_buf = NULL;
								hayes->wr_buf_cnt = 0;
								if(hayes->wr_source != 0)
									g_source_remove(hayes->wr_source);
								hayes->wr_source = 0;
								if(hayes->source != 0)
									g_source_remove(hayes->source);
								hayes->source = 0;
								if(hayes->timeout != 0)
									g_source_remove(hayes->timeout);
								hayes->timeout = 0;
							}
							/* hayes_queue_pop */
							static int _hayes_queue_pop(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								HayesCommand * command;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s()\n", __func__);
							#endif
								if(hayes->timeout != 0)
									g_source_remove(hayes->timeout);
								hayes->timeout = 0;
								if(hayes->queue == NULL) /* nothing to send */
									return 0;
								command = hayes->queue->data; /* XXX assumes it's valid */
								_hayes_command_delete(command);
								hayes->queue = g_slist_remove(hayes->queue, command);
								return 0;
							}
							/* hayes_queue_push */
							static int _hayes_queue_push(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								HayesCommand * command;
								char const * prefix = "";
								char const * attention;
								const char suffix[2] = "\r\n";
								size_t size;
								char * p;
								if(hayes->queue == NULL) /* nothing to send */
									return 0;
								command = hayes->queue->data;
								attention = _hayes_command_get_attention(command);
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s() pushing \"%s\"\n", __func__, attention);
							#endif
								if(hayes->mode == HAYES_MODE_DATA)
							#if 0 /* FIXME does not seem to work (see ATS2, ATS12) */
									prefix = "+++\r\n";
							#else
									return 0; /* XXX ignoring commands in DATA mode */
							#endif
								size = strlen(prefix) + strlen(attention) + sizeof(suffix);
								if((p = realloc(hayes->wr_buf, hayes->wr_buf_cnt + size)) == NULL)
									return -modem->helper->error(modem->helper->modem, strerror(
												errno), 1);
								hayes->wr_buf = p;
								snprintf(&hayes->wr_buf[hayes->wr_buf_cnt], size, "%s%s%s", prefix,
										attention, suffix);
								hayes->wr_buf_cnt += size;
								_hayes_command_set_status(command, HCS_ACTIVE);
								if(hayes->channel != NULL && hayes->wr_source == 0)
									hayes->wr_source = g_io_add_watch(hayes->channel, G_IO_OUT,
											_on_watch_can_write, modem);
								if(hayes->timeout != 0)
									g_source_remove(hayes->timeout);
								if((hayes->timeout = _hayes_command_get_timeout(command)) != 0)
									hayes->timeout = g_timeout_add(hayes->timeout, _on_timeout,
											modem);
								return 0;
							}
							/* hayes_reset */
							static int _hayes_reset(ModemPlugin * modem)
							{
								Hayes * hayes = modem->priv;
								_hayes_stop(modem);
								_hayes_start(modem, hayes->retry);
								return 0;
							}
							/* commands */
							static HayesCommand * _hayes_command_new(char const * attention)
							{
								HayesCommand * command;
								if((command = object_new(sizeof(*command))) == NULL)
									return NULL;
								command->priority = HCP_NORMAL;
								command->status = HCS_PENDING;
								command->attention = string_new(attention);
								command->timeout = 30000;
								command->callback = NULL;
								command->priv = NULL;
								command->answer = NULL;
								if(command->attention == NULL)
								{
									_hayes_command_delete(command);
									return NULL;
								}
								return command;
							}
							/* hayes_command_delete */
							static void _hayes_command_delete(HayesCommand * command)
							{
								string_delete(command->attention);
								string_delete(command->answer);
								object_delete(command);
							}
							/* hayes_command_get_answer */
							static char const * _hayes_command_get_answer(HayesCommand * command)
							{
								return command->answer;
							}
							/* hayes_command_get_attention */
							static char const * _hayes_command_get_attention(HayesCommand * command)
							{
								return command->attention;
							}
							#if 0 /* XXX no longer being used */
							/* hayes_command_get_line */
							static char * _hayes_command_get_line(HayesCommand * command,
									char const * prefix)
							{
								/* FIXME also return the other lines matching */
								char * ret;
								char const * answer = command->answer;
								size_t len;
								char * p;
								if(prefix == NULL)
									return NULL;
								len = strlen(prefix);
								while(answer != NULL)
									if(strncmp(answer, prefix, len) == 0 && strncmp(&answer[len],
												": ", 2) == 0)
									{
										if((ret = string_new(&answer[len + 2])) != NULL
												&& (p = strchr(ret, '\n')) != NULL)
											*p = '\0';
										return ret;
									}
									else if((answer = strchr(answer, '\n')) != NULL)
										answer++;
								return NULL;
							}
							#endif
							/* hayes_command_get_status */
							static HayesCommandStatus _hayes_command_get_status(HayesCommand * command)
							{
								return command->status;
							}
							/* hayes_command_get_timeout */
							static unsigned int _hayes_command_get_timeout(HayesCommand * command)
							{
								return command->timeout;
							}
							/* hayes_command_set_callback */
							static void _hayes_command_set_callback(HayesCommand * command,
									HayesCommandCallback callback, void * priv)
							{
								command->callback = callback;
								command->priv = priv;
							}
							/* hayes_command_set_priority */
							static void _hayes_command_set_priority(HayesCommand * command,
									HayesCommandPriority priority)
							{
								command->priority = priority;
							}
							/* hayes_command_set_status */
							static void _hayes_command_set_status(HayesCommand * command,
									HayesCommandStatus status)
							{
								command->status = status;
							}
							/* hayes_command_set_timeout */
							static void _hayes_command_set_timeout(HayesCommand * command,
									unsigned int timeout)
							{
								command->timeout = timeout;
							}
							/* hayes_command_answer_append */
							static int _hayes_command_answer_append(HayesCommand * command,
									char const * answer)
							{
								char * p;
								if(answer == NULL)
									return 0;
								if(command->answer == NULL)
									p = string_new_append(answer, "\n", NULL);
								else
									p = string_new_append(command->answer, answer, "\n", NULL);
								if(p == NULL)
									return -1;
								free(command->answer);
								command->answer = p;
								return 0;
							}
							/* hayes_command_callback */
							static HayesCommandStatus _hayes_command_callback(HayesCommand * command)
							{
								if(command->callback == NULL)
								{
									if(command->status == HCS_ACTIVE)
										/* we don't expect any answer */
										command->status = HCS_SUCCESS;
								}
								else
									command->status = command->callback(command, command->status,
											command->priv);
								return command->status;
							}
							/* callbacks */
							/* on_reset */
							static int _reset_open(ModemPlugin * modem);
							static int _reset_configure(ModemPlugin * modem, char const * device, int fd);
							static unsigned int _reset_configure_baudrate(ModemPlugin * modem,
									unsigned int baudrate);
							static gboolean _reset_settle(gpointer data);
							static HayesCommandStatus _on_reset_callback(HayesCommand * command,
									HayesCommandStatus status, void * priv);
							static gboolean _on_reset(gpointer data)
							{
								ModemPlugin * modem = data;
								Hayes * hayes = modem->priv;
								GError * error = NULL;
								int fd;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s()\n", __func__);
							#endif
								_hayes_stop(modem);
								if((fd = _reset_open(modem)) < 0)
								{
									modem->helper->error(NULL, error_get(), 1);
									if(hayes->retry > 0)
										hayes->source = g_timeout_add(hayes->retry, _on_reset,
												modem);
									return FALSE;
								}
								hayes->channel = g_io_channel_unix_new(fd);
								if((g_io_channel_set_encoding(hayes->channel, NULL, &error))
										!= G_IO_STATUS_NORMAL)
								{
									modem->helper->error(modem->helper->modem, error->message, 1);
									g_error_free(error);
								}
								g_io_channel_set_buffered(hayes->channel, FALSE);
								hayes->rd_source = g_io_add_watch(hayes->channel, G_IO_IN,
										_on_watch_can_read, modem);
								_reset_settle(modem);
								return FALSE;
							}
							static int _reset_open(ModemPlugin * modem)
							{
								char const * device = modem->config[HAYES_CONFIG_DEVICE].value;
								int fd;
								if(device == NULL)
									device = "/dev/modem";
								if((fd = open(device, O_RDWR | O_NONBLOCK)) < 0)
									return -error_set_code(1, "%s: %s", device, strerror(errno));
								if(_reset_configure(modem, device, fd) != 0)
								{
									close(fd);
									return -1;
								}
								return fd;
							}
							static int _reset_configure(ModemPlugin * modem, char const * device, int fd)
							{
								unsigned int baudrate = (unsigned long)modem->config[
									HAYES_CONFIG_BAUDRATE].value;
								unsigned int hwflow = (modem->config[HAYES_CONFIG_HWFLOW].value
										!= NULL) ? 1 : 0;
								struct stat st;
								int fl;
								struct termios term;
								baudrate = _reset_configure_baudrate(modem, baudrate);
								if(flock(fd, LOCK_EX | LOCK_NB) != 0)
									return 1;
								fl = fcntl(fd, F_GETFL, 0);
								if(fcntl(fd, F_SETFL, fl & ~O_NONBLOCK) == -1)
									return 1;
								if(fstat(fd, &st) != 0)
									return 1;
								if(st.st_mode & S_IFCHR) /* character special */
								{
									if(tcgetattr(fd, &term) != 0)
										return 1;
									term.c_cflag &= ~(CSIZE | PARENB);
									term.c_cflag |= CS8;
									term.c_cflag |= CREAD;
									term.c_cflag |= CLOCAL;
									if(hwflow != 0)
										term.c_cflag |= CRTSCTS;
									else
										term.c_cflag &= ~CRTSCTS;
									term.c_iflag = (IGNPAR | IGNBRK);
									term.c_lflag = 0;
									term.c_oflag = 0;
									term.c_cc[VMIN] = 1;
									term.c_cc[VTIME] = 0;
									if(cfsetispeed(&term, 0) != 0) /* same speed as output speed */
										error_set("%s", device); /* go on anyway */
									if(cfsetospeed(&term, baudrate) != 0)
										error_set("%s", device); /* go on anyway */
									if(tcsetattr(fd, TCSAFLUSH, &term) != 0)
										return 1;
								}
								return 0;
							}
							static unsigned int _reset_configure_baudrate(ModemPlugin * modem,
									unsigned int baudrate)
							{
								switch(baudrate)
								{
									case 1200:
										return B1200;
									case 2400:
										return B2400;
									case 4800:
										return B4800;
									case 9600:
										return B9600;
									case 19200:
										return B19200;
									case 38400:
										return B38400;
							#ifdef B76800
									case 76800:
										return B76800;
							#endif
							#ifdef B14400
									case 14400:
										return B14400;
							#endif
							#ifdef B28800
									case 28800:
										return B28800;
							#endif
									case 57600:
										return B57600;
									case 115200:
										return B115200;
									case 230400:
										return B230400;
									case 460800:
										return B460800;
									case 921600:
										return B921600;
									default:
										error_set("%u%s", baudrate,
												"Unsupported baudrate (using 115200)");
										modem->helper->error(NULL, error_get(), 1);
										return B115200;
								}
							}
							static gboolean _reset_settle(gpointer data)
							{
								ModemPlugin * modem = data;
								HayesCommand * command;
								if((command = _hayes_command_new("ATZE0V1")) == NULL)
								{
									modem->helper->error(modem->helper->modem, error_get(), 1);
									return FALSE;
								}
								_hayes_command_set_callback(command, _on_reset_callback, modem);
								_hayes_command_set_priority(command, HCP_IMMEDIATE);
								_hayes_command_set_timeout(command, 500);
								if(_hayes_queue_command(modem, command) != 0)
								{
									modem->helper->error(modem->helper->modem, error_get(), 1);
									_hayes_command_delete(command);
								}
								return FALSE;
							}
							static HayesCommandStatus _on_reset_callback(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_STATUS];
								ModemRequest request;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s()\n", __func__);
							#endif
								if(hayes->timeout != 0)
									g_source_remove(hayes->timeout);
								hayes->timeout = 0;
								switch(status)
								{
									case HCS_PENDING: /* should not happen */
									case HCS_QUEUED:
									case HCS_SUCCESS:
									case HCS_ERROR:
										break;
									case HCS_ACTIVE:
										/* a reply was obtained */
										status = _on_request_generic(command, status, modem);
										if(status != HCS_SUCCESS && status != HCS_ERROR)
											return HCS_ACTIVE;
										_hayes_set_mode(modem, HAYES_MODE_COMMAND);
										request.type = HAYES_REQUEST_LOCAL_ECHO_DISABLE;
										_hayes_request(modem, &request);
										request.type = HAYES_REQUEST_VERBOSE_ENABLE;
										_hayes_request(modem, &request);
										request.type = HAYES_REQUEST_MODEL;
										_hayes_request(modem, &request);
										request.type = HAYES_REQUEST_EXTENDED_ERRORS;
										_hayes_request(modem, &request);
										request.type = HAYES_REQUEST_FUNCTIONAL;
										_hayes_request(modem, &request);
										/* report being online */
										event->status.online = 1;
										modem->helper->event(modem->helper->modem, event);
										return HCS_SUCCESS;
									case HCS_TIMEOUT:
										/* try again */
										_reset_settle(modem);
										break;
								}
								return HCS_ERROR; /* destroy and queue again */
							}
							/* on_timeout */
							static gboolean _on_timeout(gpointer data)
							{
								ModemPlugin * modem = data;
								Hayes * hayes = modem->priv;
								HayesCommand * command;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s()\n", __func__);
							#endif
								hayes->timeout = 0;
								if(hayes->queue == NULL || (command = hayes->queue->data) == NULL)
									return FALSE;
								_hayes_command_set_status(command, HCS_TIMEOUT);
								_hayes_command_callback(command);
								_hayes_queue_pop(modem);
								_hayes_queue_push(modem);
								return FALSE;
							}
							/* on_watch_can_read */
							static gboolean _on_watch_can_read(GIOChannel * source, GIOCondition condition,
									gpointer data)
							{
								ModemPlugin * modem = data;
								ModemPluginHelper * helper = modem->helper;
								Hayes * hayes = modem->priv;
								gsize cnt = 0;
								GError * error = NULL;
								GIOStatus status;
								char * p;
								if(condition != G_IO_IN || source != hayes->channel)
									return FALSE; /* should not happen */
								if((p = realloc(hayes->rd_buf, hayes->rd_buf_cnt + 256)) == NULL)
									return TRUE; /* XXX retries immediately (delay?) */
								hayes->rd_buf = p;
								status = g_io_channel_read_chars(source,
										&hayes->rd_buf[hayes->rd_buf_cnt], 256, &cnt, &error);
							#ifdef DEBUG
								fprintf(stderr, "%s", "DEBUG: MODEM: ");
								fwrite(&hayes->rd_buf[hayes->rd_buf_cnt], sizeof(*p), cnt, stderr);
							#endif
								hayes->rd_buf_cnt += cnt;
								switch(status)
								{
									case G_IO_STATUS_NORMAL:
										break;
									case G_IO_STATUS_ERROR:
										helper->error(helper->modem, error->message, 1);
										g_error_free(error);
									case G_IO_STATUS_EOF:
									default: /* should not happen... */
										if(hayes->retry > 0)
											_hayes_reset(modem);
										hayes->rd_source = 0;
										return FALSE;
								}
								switch(hayes->mode)
								{
									case HAYES_MODE_INIT:
									case HAYES_MODE_COMMAND:
										_hayes_parse(modem);
										break;
									case HAYES_MODE_DATA:
										if(hayes->wr_ppp_channel == NULL
												|| hayes->wr_ppp_source != 0)
											break;
										hayes->wr_ppp_source = g_io_add_watch(
												hayes->wr_ppp_channel, G_IO_OUT,
												_on_watch_can_write_ppp, modem);
										break;
								}
								return TRUE;
							}
							/* on_watch_can_read_ppp */
							static gboolean _on_watch_can_read_ppp(GIOChannel * source,
									GIOCondition condition, gpointer data)
							{
								ModemPlugin * modem = data;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CONNECTION];
								gsize cnt = 0;
								GError * error = NULL;
								GIOStatus status;
								char * p;
								if(condition != G_IO_IN || source != hayes->rd_ppp_channel)
									return FALSE; /* should not happen */
								if((p = realloc(hayes->wr_buf, hayes->wr_buf_cnt + 256)) == NULL)
									return TRUE; /* XXX retries immediately (delay?) */
								hayes->wr_buf = p;
								status = g_io_channel_read_chars(source,
										&hayes->wr_buf[hayes->wr_buf_cnt], 256, &cnt, &error);
								hayes->wr_buf_cnt += cnt;
								event->connection.out += cnt;
								switch(status)
								{
									case G_IO_STATUS_NORMAL:
										break;
									case G_IO_STATUS_ERROR:
										modem->helper->error(modem->helper->modem,
												error->message, 1);
										g_error_free(error);
									case G_IO_STATUS_EOF:
									default:
										hayes->rd_ppp_source = 0;
										event->connection.connected = 0;
										modem->helper->event(modem->helper->modem, event);
										_hayes_set_mode(modem, HAYES_MODE_INIT);
										return FALSE;
								}
								if(hayes->channel != NULL && hayes->wr_source == 0)
									hayes->wr_source = g_io_add_watch(hayes->channel, G_IO_OUT,
											_on_watch_can_write, modem);
								return TRUE;
							}
							/* on_watch_can_write */
							static gboolean _on_watch_can_write(GIOChannel * source, GIOCondition condition,
									gpointer data)
							{
								ModemPlugin * modem = data;
								Hayes * hayes = modem->priv;
								gsize cnt = 0;
								GError * error = NULL;
								GIOStatus status;
								char * p;
								if(condition != G_IO_OUT || source != hayes->channel)
									return FALSE; /* should not happen */
								status = g_io_channel_write_chars(source, hayes->wr_buf,
										hayes->wr_buf_cnt, &cnt, &error);
							#ifdef DEBUG
								fprintf(stderr, "%s", "DEBUG: PHONE: ");
								fwrite(hayes->wr_buf, sizeof(*p), cnt, stderr);
							#endif
								if(cnt != 0) /* some data may have been written anyway */
								{
									hayes->wr_buf_cnt -= cnt;
									memmove(hayes->wr_buf, &hayes->wr_buf[cnt], hayes->wr_buf_cnt);
									if((p = realloc(hayes->wr_buf, hayes->wr_buf_cnt)) != NULL)
										hayes->wr_buf = p; /* we can ignore errors... */
									else if(hayes->wr_buf_cnt == 0)
										hayes->wr_buf = NULL; /* ...except when it's not one */
								}
								switch(status)
								{
									case G_IO_STATUS_NORMAL:
										break;
									case G_IO_STATUS_ERROR:
										modem->helper->error(modem->helper->modem,
												error->message, 1);
										g_error_free(error);
									case G_IO_STATUS_EOF:
									default: /* should not happen */
										hayes->wr_source = 0;
										if(hayes->retry > 0)
											_hayes_reset(modem);
										return FALSE;
								}
								if(hayes->wr_buf_cnt > 0) /* there is more data to write */
									return TRUE;
								hayes->wr_source = 0;
								return FALSE;
							}
							/* on_watch_can_write_ppp */
							static gboolean _on_watch_can_write_ppp(GIOChannel * source,
									GIOCondition condition, gpointer data)
							{
								ModemPlugin * modem = data;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CONNECTION];
								gsize cnt = 0;
								GError * error = NULL;
								GIOStatus status;
								char * p;
								if(condition != G_IO_OUT || source != hayes->wr_ppp_channel)
									return FALSE; /* should not happen */
								status = g_io_channel_write_chars(source, hayes->rd_buf,
										hayes->rd_buf_cnt, &cnt, &error);
								event->connection.in += cnt;
								if(cnt != 0) /* some data may have been written anyway */
								{
									hayes->rd_buf_cnt -= cnt;
									memmove(hayes->rd_buf, &hayes->rd_buf[cnt], hayes->rd_buf_cnt);
									if((p = realloc(hayes->rd_buf, hayes->rd_buf_cnt)) != NULL)
										hayes->rd_buf = p; /* we can ignore errors... */
									else if(hayes->rd_buf_cnt == 0)
										hayes->rd_buf = NULL; /* ...except when it's not one */
								}
								switch(status)
								{
									case G_IO_STATUS_NORMAL:
										break;
									case G_IO_STATUS_ERROR:
										modem->helper->error(modem->helper->modem,
												error->message, 1);
										g_error_free(error);
									case G_IO_STATUS_EOF:
									default:
										hayes->wr_ppp_source = 0;
										event->connection.connected = 0;
										modem->helper->event(modem->helper->modem, event);
										_hayes_set_mode(modem, HAYES_MODE_INIT);
										return FALSE;
								}
								if(hayes->rd_buf_cnt > 0) /* there is more data to write */
									return TRUE;
								hayes->wr_ppp_source = 0;
								return FALSE;
							}
							/* on_request_authenticate */
							static HayesCommandStatus _on_request_authenticate(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_AUTHENTICATION];
								ModemRequest request;
								memset(&request, 0, sizeof(&request));
								switch((status = _on_request_generic(command, status, priv)))
								{
									case HCS_ERROR:
										event->authentication.status
											= MODEM_AUTHENTICATION_STATUS_ERROR;
										break;
									case HCS_SUCCESS:
										break;
									default:
										return status;
								}
								if(event->authentication.name != NULL)
									modem->helper->event(modem->helper->modem, event);
								if(status == HCS_SUCCESS)
								{
									request.type = HAYES_REQUEST_SIM_PIN_VALID;
									_hayes_request(modem, &request);
								}
								return status;
							}
							/* on_request_battery_level */
							static HayesCommandStatus _on_request_battery_level(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_BATTERY_LEVEL];
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
									return status;
								modem->helper->event(modem->helper->modem, event);
								return status;
							}
							/* on_request_call */
							static HayesCommandStatus _on_request_call(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
									return status;
								modem->helper->event(modem->helper->modem, event);
								return status;
							}
							/* on_request_call_incoming */
							static HayesCommandStatus _on_request_call_incoming(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS
										&& status != HCS_ERROR)
									return status;
								event->call.direction = MODEM_CALL_DIRECTION_INCOMING;
								event->call.status = (status == HCS_SUCCESS)
									? MODEM_CALL_STATUS_ACTIVE : MODEM_CALL_STATUS_NONE;
								modem->helper->event(modem->helper->modem, event);
								return status;
							}
							/* on_request_call_outgoing */
							static HayesCommandStatus _on_request_call_outgoing(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS
										&& status != HCS_ERROR)
									return status;
								event->call.direction = MODEM_CALL_DIRECTION_OUTGOING;
								event->call.status = (status == HCS_SUCCESS)
									? MODEM_CALL_STATUS_ACTIVE : MODEM_CALL_STATUS_NONE;
								modem->helper->event(modem->helper->modem, event);
								return status;
							}
							/* on_request_call_status */
							static HayesCommandStatus _on_request_call_status(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS
										&& status != HCS_ERROR)
									return status;
								_hayes_trigger(modem, MODEM_EVENT_TYPE_CALL);
								return status;
							}
							/* on_request_contact_delete */
							static HayesCommandStatus _on_request_contact_delete(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CONTACT_DELETED];
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
									return status;
								modem->helper->event(modem->helper->modem, event);
								return status;
							}
							/* on_request_contact_list */
							static HayesCommandStatus _on_request_contact_list(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								/* FIXME implement */
								return _on_request_generic(command, status, priv);
							}
							/* on_request_functional */
							static HayesCommandStatus _on_request_functional(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								ModemRequest request;
								memset(&request, 0, sizeof(request));
								switch((status = _on_request_generic(command, status, priv)))
								{
									case HCS_ERROR:
										/* try to enable */
										request.type = HAYES_REQUEST_FUNCTIONAL_ENABLE;
										_hayes_request(modem, &request);
										break;
									default:
										break;
								}
								return status;
							}
							/* on_request_functional_enable */
							static HayesCommandStatus _on_request_functional_enable(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								ModemRequest request;
								memset(&request, 0, sizeof(request));
								switch((status = _on_request_generic(command, status, priv)))
								{
									case HCS_SUCCESS:
										_on_trigger_cfun(modem, "1"); /* XXX ugly workaround */
										break;
									case HCS_TIMEOUT:
										/* repeat request */
										request.type = HAYES_REQUEST_FUNCTIONAL_ENABLE;
										_hayes_request(modem, &request);
										break;
									default:
										break;
								}
								return status;
							}
							/* on_request_generic */
							static HayesCommandStatus _on_request_generic(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								char const * answer;
								if(status != HCS_ACTIVE) /* XXX should not happen */
									return HCS_ERROR;
								if((answer = _hayes_command_get_answer(command)) == NULL)
									return HCS_ERROR;
								while(answer != NULL)
									/* FIXME also handle BUSY/NO CARRIER/CONNECT/etc */
									if(strncmp(answer, "OK\n", 3) == 0)
										return HCS_SUCCESS;
									else if(strncmp(answer, "ERROR\n", 6) == 0)
										return HCS_ERROR;
									else if((answer = strchr(answer, '\n')) != NULL)
										answer++;
								return HCS_ACTIVE;
							}
							/* on_request_message */
							static HayesCommandStatus _on_request_message(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								/* FIXME implement */
								return _on_request_generic(command, status, priv);
							}
							/* on_request_message_delete */
							static HayesCommandStatus _on_request_message_delete(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MESSAGE_DELETED];
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
									return status;
								modem->helper->event(modem->helper->modem, event);
								return status;
							}
							/* on_request_message_list */
							static HayesCommandStatus _on_request_message_list(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								/* FIXME implement */
								return _on_request_generic(command, status, priv);
							}
							/* on_request_message_send */
							static HayesCommandStatus _on_request_message_send(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								/* FIXME implement */
								return _on_request_generic(command, status, priv);
							}
							/* on_request_model */
							static HayesCommandStatus _on_request_model(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
									return status;
								modem->helper->event(modem->helper->modem, event);
								return status;
							}
							/* on_request_sim_pin_valid */
							static HayesCommandStatus _on_request_sim_pin_valid(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								ModemPlugin * modem = priv;
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_AUTHENTICATION];
								ModemRequest request;
								if((status = _on_request_generic(command, status, priv)) != HCS_SUCCESS)
									return status;
								modem->helper->event(modem->helper->modem, event);
								/* return if not successful */
								if(event->authentication.status != MODEM_AUTHENTICATION_STATUS_OK)
									return status;
								/* automatically register */
								/* XXX should really wait for the telephony application for this */
								memset(&request, 0, sizeof(&request));
								request.type = HAYES_REQUEST_OPERATOR_FORMAT_LONG;
								_hayes_request(modem, &request);
								request.type = HAYES_REQUEST_REGISTRATION_UNSOLLICITED_ENABLE;
								_hayes_request(modem, &request);
								request.type = MODEM_REQUEST_REGISTRATION;
								request.registration.mode = MODEM_REGISTRATION_MODE_AUTOMATIC;
								_hayes_request(modem, &request);
								/* force a registration report */
								request.type = HAYES_REQUEST_REGISTRATION;
								_hayes_request(modem, &request);
								/* report new messages */
								request.type = HAYES_REQUEST_MESSAGE_UNSOLLICITED_ENABLE;
								_hayes_request(modem, &request);
								/* report new notifications */
								request.type = HAYES_REQUEST_SUPPLEMENTARY_SERVICE_DATA_ENABLE;
								_hayes_request(modem, &request);
								/* refresh the current call status */
								_hayes_trigger(modem, MODEM_EVENT_TYPE_CALL);
								/* refresh the contact list */
								request.type = MODEM_REQUEST_CONTACT_LIST;
								_hayes_request(modem, &request);
								/* refresh the message list */
								request.type = MODEM_REQUEST_MESSAGE_LIST;
								_hayes_request(modem, &request);
								return status;
							}
							/* on_request_unsupported */
							static HayesCommandStatus _on_request_unsupported(HayesCommand * command,
									HayesCommandStatus status, void * priv)
							{
								/* FIXME report an unsupported event with the result of the command */
								return _on_request_generic(command, status, priv);
							}
							/* on_trigger_call_error */
							static void _on_trigger_call_error(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								HayesCommand * command = (hayes->queue != NULL) ? hayes->queue->data
									: NULL;
								if(command != NULL)
									_hayes_command_set_status(command, HCS_ERROR);
								_hayes_trigger(modem, MODEM_EVENT_TYPE_CALL);
							}
							/* on_trigger_cbc */
							static void _on_trigger_cbc(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_BATTERY_LEVEL];
								int res;
								unsigned int u;
								unsigned int v;
								double f;
								if((res = sscanf(answer, "%u,%u", &u, &v)) != 2)
									return;
								event->battery_level.status = MODEM_BATTERY_STATUS_UNKNOWN;
								if(u == 0)
									u = MODEM_BATTERY_STATUS_CONNECTED;
								else if(u == 1)
									u = MODEM_BATTERY_STATUS_CHARGING;
								else if(u == 2)
									u = MODEM_BATTERY_STATUS_NONE;
								else if(u == 3)
									u = MODEM_BATTERY_STATUS_ERROR;
								else
									u = MODEM_BATTERY_STATUS_UNKNOWN;
								switch((event->battery_level.status = u))
								{
									case MODEM_BATTERY_STATUS_CONNECTED:
									case MODEM_BATTERY_STATUS_CHARGING:
										f = v;
										if(hayes->quirks & HAYES_QUIRK_BATTERY_70)
											f /= 70.0;
										else
											f /= 100.0;
										f = max(f, 0.0);
										event->battery_level.level = min(f, 1.0);
										break;
									default:
										event->battery_level.level = 0.0 / 0.0;
										break;
								}
							}
							/* on_trigger_cfun */
							static void _on_trigger_cfun(ModemPlugin * modem, char const * answer)
							{
								unsigned int u;
								ModemRequest request;
								if(sscanf(answer, "%u", &u) != 1)
									return;
								memset(&request, 0, sizeof(request));
								if(u != 1)
								{
									/* FIXME only enable if requested to */
									request.type = HAYES_REQUEST_FUNCTIONAL_ENABLE;
									return;
								}
								request.type = HAYES_REQUEST_EXTENDED_RING_REPORTS;
								_hayes_request(modem, &request);
								request.type = MODEM_REQUEST_CALL_PRESENTATION;
								request.call_presentation.enabled = 1;
								_hayes_request(modem, &request);
								request.type = HAYES_REQUEST_CALL_WAITING_UNSOLLICITED_ENABLE;
								_hayes_request(modem, &request);
								request.type = HAYES_REQUEST_CONNECTED_LINE_ENABLE;
								_hayes_request(modem, &request);
								request.type = HAYES_REQUEST_SIM_PIN_VALID;
								_hayes_request(modem, &request);
							}
							/* on_trigger_cgatt */
							static void _on_trigger_cgatt(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
								unsigned int u;
								if(sscanf(answer, "%u", &u) != 1)
									return;
								free(hayes->registration_media);
								if(u == 1)
									hayes->registration_media = strdup("GPRS");
								else
									hayes->registration_media = NULL;
								event->registration.media = hayes->registration_media;
							}
							/* on_trigger_cgmi */
							static void _on_trigger_cgmi(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];
								char * p;
								if((p = strdup(answer)) == NULL)
									return; /* XXX report error? */
								free(hayes->model_vendor);
								hayes->model_vendor = p;
								event->model.vendor = p;
							}
							/* on_trigger_cgmm */
							static void _on_trigger_cgmm(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];
								char * p;
								size_t i;
								if((p = strdup(answer)) == NULL)
									return; /* XXX report error? */
								free(hayes->model_name);
								hayes->model_name = p;
								event->model.name = p;
								/* determine known quirks */
								for(i = 0; _hayes_quirks[i].model != NULL; i++)
									if(strcmp(_hayes_quirks[i].model, p) == 0)
									{
										hayes->quirks = _hayes_quirks[i].quirks;
							#ifdef DEBUG
										fprintf(stderr, "DEBUG: %s() quirks=%u\n", __func__,
												hayes->quirks);
							#endif
										break;
									}
							}
							/* on_trigger_cgmr */
							static void _on_trigger_cgmr(ModemPlugin * modem, char const * answer)
								/* FIXME the output may be multi-line */
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MODEL];
								char * p;
								if((p = strdup(answer)) == NULL)
									return; /* XXX report error? */
								free(hayes->model_version);
								hayes->model_version = p;
								event->model.version = p;
							}
							/* on_trigger_clip */
							static void _on_trigger_clip(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								char buf[32];
								unsigned int u;
								if(sscanf(answer, "\"%31[^\"]\",%u", buf, &u) != 2)
									return;
								buf[sizeof(buf) - 1] = '\0';
								free(hayes->call_number);
								switch(u)
								{
									case 145:
										if((hayes->call_number = malloc(sizeof(buf) + 1))
												== NULL)
											break;
										snprintf(hayes->call_number, sizeof(buf) + 1, "%s%s",
												"+", buf);
										break;
									default:
										hayes->call_number = strdup(buf);
										break;
								}
								/* this is always an unsollicited event */
								modem->helper->event(modem->helper->modem, event);
							}
							/* on_trigger_cme_error */
							static void _on_trigger_cme_error(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								HayesCommand * command = (hayes->queue != NULL) ? hayes->queue->data
									: NULL;
								unsigned int u;
								if(command != NULL)
									_hayes_command_set_status(command, HCS_ERROR);
								if(sscanf(answer, "%u", &u) != 1)
									return;
								switch(u)
								{
									case 11: /* SIM PIN required */
									case 12: /* SIM PUK required */
										_hayes_trigger(modem, MODEM_EVENT_TYPE_AUTHENTICATION);
										break;
									default: /* FIXME implement the rest */
									case 16: /* Incorrect SIM PUK */
										break;
								}
							}
							/* on_trigger_cmgl */
							static void _on_trigger_cmgl(ModemPlugin * modem, char const * answer)
							{
								ModemRequest request;
								unsigned int id;
								unsigned int u;
								/* XXX we could already be reading the message at this point */
								if(sscanf(answer, "%u,%u,%u,%u", &id, &u, &u, &u) != 4
										&& sscanf(answer, "%u,%u,,%u", &id, &u, &u) != 3)
									/* XXX we may be stuck in PDU mode at this point */
									return;
								request.type = MODEM_REQUEST_MESSAGE;
								request.message.id = id;
								_hayes_request(modem, &request);
							}
							/* on_trigger_cmgr */
							static char * _cmgr_pdu_parse(char const * pdu, time_t * timestamp,
									char * number, ModemMessageEncoding * encoding,
									size_t * length);
							static char * _cmgr_pdu_parse_encoding_data(char const * pdu, size_t len,
									size_t i, size_t hdr, ModemMessageEncoding * encoding,
									size_t * length);
							static char * _cmgr_pdu_parse_encoding_default(char const * pdu, size_t len,
							                size_t i, size_t hdr, ModemMessageEncoding * encoding,
									size_t * length);
							static void _cmgr_pdu_parse_number(unsigned int type, char const * number,
									size_t length, char * buf);
							static time_t _cmgr_pdu_parse_timestamp(char const * timestamp);
							static void _on_trigger_cmgr(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MESSAGE];
								char buf[32];
								char number[32];
								char date[32];
								struct tm t;
								unsigned int mbox;
								unsigned int alpha = 0;
								unsigned int length;
								char * p;
								/* text mode support */
								if(sscanf(answer, "\"%31[^\"]\",\"%31[^\"]\",,\"%31[^\"]\"", buf,
											number, date) == 3)
								{
									number[sizeof(number) - 1] = '\0';
									string_delete(hayes->message_number);
									hayes->message_number = string_new(number);
									event->message.number = hayes->message_number;
									date[sizeof(date) - 1] = '\0';
									if(strptime(date, "%y/%m/%d,%H:%M:%S", &t) == NULL)
										/* XXX also parse the timezone? */
										localtime_r(NULL, &t);
									event->message.date = mktime(&t);
									event->message.length = 0;
									return; /* we need to wait for the next line */
								}
								/* PDU mode support */
								if(sscanf(answer, "%u,%u,%u", &mbox, &alpha, &length) == 3
										|| sscanf(answer, "%u,,%u", &mbox, &length) == 2)
									return; /* we need to wait for the next line */
								/* message content */
								if(event->message.length == 0) /* XXX assumes this is text mode */
								{
									event->message.encoding = MODEM_MESSAGE_ENCODING_UTF8;
									event->message.content = answer;
									event->message.length = strlen(answer);
									modem->helper->event(modem->helper->modem, event);
									return;
								}
								if((p = _cmgr_pdu_parse(answer, &event->message.date, number,
												&event->message.encoding,
												&event->message.length)) == NULL)
									return;
								event->message.number = number; /* XXX */
								modem->helper->event(modem->helper->modem, event);
								free(p);
							}
							static char * _cmgr_pdu_parse(char const * pdu, time_t * timestamp,
									char * number, ModemMessageEncoding * encoding, size_t * length)
							{
								size_t len;
								unsigned int smscl;
								unsigned int tp;
								unsigned int hdr;
								unsigned int addrl;
								unsigned int pid;
								unsigned int dcs;
								unsigned int datal;
								unsigned int u;
								char const * q;
								size_t i;
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, pdu);
							#endif
								len = strlen(pdu);
								if(sscanf(pdu, "%02X", &smscl) != 1) /* SMSC length */
									return NULL;
								if((smscl * 2) + 2 > len)
									return NULL;
								q = pdu + (smscl * 2) + 2;
								if(sscanf(q, "%02X", &tp) != 1)
									return NULL;
								if((tp & 0x03) != 0x00) /* TP-MTI not SMS-DELIVER */
									return NULL;
								hdr = ((tp & 0x40) == 0x40) ? 1 : 0; /* TP-UDHI header present */
								if((smscl * 2) + 4 > len)
									return NULL;
								q = pdu + (smscl * 2) + 4;
								if(sscanf(q, "%02X", &addrl) != 1) /* address length */
									return NULL;
								if((smscl * 2) + 6 > len)
									return NULL;
								q = pdu + (smscl * 2) + 6;
								if(sscanf(q, "%02X", &u) != 1) /* type of address */
									return NULL;
								/* FIXME this probably depends on the type of address */
								if(addrl % 2 == 1)
									addrl++;
								if((smscl * 2) + 2 + 4 + addrl + 2 > len)
									return NULL;
								_cmgr_pdu_parse_number(u, q + 2, addrl, number);
								q = pdu + (smscl * 2) + 2 + 4 + addrl + 2;
								if(sscanf(q, "%02X", &pid) != 1) /* PID */
									return NULL;
								if((smscl * 2) + 2 + 4 + addrl + 4 > len)
									return NULL;
								q = pdu + (smscl * 2) + 2 + 4 + addrl + 4;
								if(sscanf(q, "%02X", &dcs) != 1) /* DCS */
									return NULL;
								if((smscl * 2) + 2 + 4 + addrl + 6 > len)
									return NULL;
								q = pdu + (smscl * 2) + 2 + 4 + addrl + 6;
								if(timestamp != NULL)
									*timestamp = _cmgr_pdu_parse_timestamp(q);
								if((smscl * 2) + 2 + 4 + addrl + 6 + 14 > len)
									return NULL;
								q = pdu + (smscl * 2) + 2 + 4 + addrl + 6 + 14;
								if(sscanf(q, "%02X", &datal) != 1) /* data length */
									return NULL;
								/* XXX check the data length */
								if((i = (smscl * 2) + 2 + 4 + addrl + 6 + 16) > len)
									return NULL;
								if(hdr != 0 && sscanf(&pdu[i], "%02X", &hdr) != 1)
									return NULL;
								if(dcs == 0x00)
									return _cmgr_pdu_parse_encoding_default(pdu, len, i, hdr,
											encoding, length);
								if(dcs == 0x04)
									return _cmgr_pdu_parse_encoding_data(pdu, len, i, hdr,
											encoding, length);
								return NULL;
							}
							static char * _cmgr_pdu_parse_encoding_data(char const * pdu, size_t len,
									size_t i, size_t hdr, ModemMessageEncoding * encoding,
									size_t * length)
							{
								unsigned char * p;
								size_t j;
								unsigned int u;
								if((p = malloc(len - i + 1)) == NULL) /* XXX 2 times big enough? */
									return NULL;
								/* FIXME actually parse the header */
								if(hdr != 0)
									i += 2 + (hdr * 2);
								for(j = 0; i + 1 < len; i+=2)
								{
									if(sscanf(&pdu[i], "%02X", &u) != 1)
									{
										free(p);
										return NULL;
									}
									p[j++] = u;
								}
								*encoding = MODEM_MESSAGE_ENCODING_DATA;
								*length = j;
								p[j] = '\0';
								return (char *)p;
							}
							static char * _cmgr_pdu_parse_encoding_default(char const * pdu, size_t len,
							                size_t i, size_t hdr, ModemMessageEncoding * encoding,
									size_t * length)
							{
								unsigned char * p;
								size_t j;
								unsigned char rest;
								int shift = 0;
								char const * q;
								unsigned int u;
								unsigned char byte;
								char * r;
								if((p = malloc(len - i + 1)) == NULL)
									return NULL;
								if(hdr != 0)
								{
									/* FIXME actually parse the header */
									u = 2 + (hdr * 2);
									if(u % 7 != 0) /* fill bits */
										u += 7 - (u % 7);
									i += u;
								}
								p[0] = '\0';
								for(j = 0, rest = 0; i + 1 < len; i+=2)
								{
									q = &pdu[i];
									if(sscanf(q, "%02X", &u) != 1)
										break; /* FIXME report an error instead? */
									byte = u;
									p[j] = (byte << (shift + 1) >> (shift + 1) << shift) & 0x7f;
									p[j] |= rest;
									p[j] = _hayes_convert_char_to_iso(p[j]);
									j++;
									rest = (byte >> (7 - shift)) & 0x7f;
									if(++shift == 7)
									{
										shift = 0;
										p[j++] = rest;
										rest = 0;
									}
								}
								*encoding = MODEM_MESSAGE_ENCODING_UTF8;
								if((r = g_convert((char *)p, j, "UTF-8", "ISO-8859-1", NULL, NULL,
												NULL)) != NULL)
								{
									free(p);
									p = (unsigned char *)r;
									j = strlen(r);
								}
								*length = j;
								return (char *)p;
							}
							static void _cmgr_pdu_parse_number(unsigned int type, char const * number,
									size_t length, char * buf)
							{
								char * b = buf;
								size_t i;
								if(type == 0x91)
									*(b++) = '+';
								for(i = 0; i < length - 1 && i < 32 - 1; i+=2)
								{
									if((number[i] != 'F' && (number[i] < '0' || number[i] > '9'))
											|| number[i + 1] < '0' || number[i + 1] > '9')
										break;
									b[i] = number[i + 1];
									if((b[i + 1] = number[i]) == 'F')
										b[i + 1] = '\0';
								}
								b[i] = '\0';
							#ifdef DEBUG
								fprintf(stderr, "DEBUG: %s(\"%s\", %lu) => \"%s\"\n", __func__, number,
										(unsigned long)length, b);
							#endif
							}
							static time_t _cmgr_pdu_parse_timestamp(char const * timestamp)
							{
								char const * p = timestamp;
								size_t i;
								struct tm t;
							#ifdef DEBUG
								char buf[32];
							#endif
								if(strlen(p) < 14)
									return 0;
								for(i = 0; i < 14; i++)
									if(p[i] < '0' || p[i] > '9')
										return 0;
								memset(&t, 0, sizeof(t));
								t.tm_year = (p[0] - '0') + ((p[1] - '0') * 10);
								t.tm_year = (t.tm_year > 70) ? t.tm_year : (100 + t.tm_year);
								t.tm_mon = (p[2] - '0') + ((p[3] - '0') * 10);
								if(t.tm_mon > 0)
									t.tm_mon--;
								t.tm_mday = (p[4] - '0') + ((p[5] - '0') * 10);
								t.tm_hour = (p[6] - '0') + ((p[7] - '0') * 10);
								t.tm_min = (p[8] - '0') + ((p[9] - '0') * 10);
								t.tm_sec = (p[10] - '0') + ((p[11] - '0') * 10);
							#ifdef DEBUG
								strftime(buf, sizeof(buf), "%d/%m/%Y %H:%M:%S", &t);
								fprintf(stderr, "DEBUG: %s() => \"%s\"\n", __func__, buf);
							#endif
								return mktime(&t);
							}
							/* on_trigger_cmgs */
							static void _on_trigger_cmgs(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_MESSAGE_SENT];
								unsigned int u;
								if(sscanf(answer, "%u", &u) != 1)
									return;
								event->message_sent.id = u;
								modem->helper->event(modem->helper->modem, event);
							}
							/* on_trigger_cms_error */
							static void _on_trigger_cms_error(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								HayesCommand * command = (hayes->queue != NULL) ? hayes->queue->data
									: NULL;
								unsigned int u;
								if(command != NULL)
									_hayes_command_set_status(command, HCS_ERROR);
								if(sscanf(answer, "%u", &u) != 1)
									return;
								switch(u)
								{
									case 311: /* SIM PIN required */
									case 316: /* SIM PUK required */
										_hayes_trigger(modem, MODEM_EVENT_TYPE_AUTHENTICATION);
										break;
									default: /* FIXME implement the rest */
										break;
								}
							}
							/* on_trigger_cmti */
							static void _on_trigger_cmti(ModemPlugin * modem, char const * answer)
							{
								char buf[32];
								unsigned int u;
								ModemRequest request;
								if(sscanf(answer, "\"%31[^\"]\",%u", buf, &u) != 2)
									return;
								buf[sizeof(buf) - 1] = '\0';
								/* fetch the new message directly */
								memset(&request, 0, sizeof(request));
								request.type = MODEM_REQUEST_MESSAGE;
								request.message.id = u;
								_hayes_request(modem, &request);
							}
							/* on_trigger_connect */
							static void _on_trigger_connect(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CONNECTION];
								HayesCommand * command = (hayes->queue != NULL) ? hayes->queue->data
									: NULL;
								char * argv[] = { "/usr/sbin/pppd", "pppd", "call", "phone",
									"user", "", "password", "", NULL };
								GSpawnFlags flags = G_SPAWN_FILE_AND_ARGV_ZERO;
								int wfd;
								int rfd;
								GError * error = NULL;
								if(command != NULL) /* XXX else report error? */
									_hayes_command_set_status(command, HCS_SUCCESS);
								_hayes_set_mode(modem, HAYES_MODE_DATA);
								if(hayes->gprs_username != NULL)
									argv[5] = hayes->gprs_username;
								if(hayes->gprs_password != NULL)
									argv[7] = hayes->gprs_password;
								if(g_spawn_async_with_pipes(NULL, argv, NULL, flags, NULL, NULL, NULL,
											&wfd, &rfd, NULL, &error) == FALSE)
								{
									modem->helper->error(NULL, error->message, 1);
									g_error_free(error);
									_hayes_reset(modem);
									return;
								}
								hayes->rd_ppp_channel = g_io_channel_unix_new(rfd);
								g_io_channel_set_encoding(hayes->rd_ppp_channel, NULL, &error);
								g_io_channel_set_buffered(hayes->rd_ppp_channel, FALSE);
								hayes->rd_ppp_source = g_io_add_watch(hayes->rd_ppp_channel, G_IO_IN,
										_on_watch_can_read_ppp, modem);
								hayes->wr_ppp_channel = g_io_channel_unix_new(wfd);
								g_io_channel_set_encoding(hayes->wr_ppp_channel, NULL, &error);
								g_io_channel_set_buffered(hayes->wr_ppp_channel, FALSE);
								hayes->wr_ppp_source = 0;
								event->connection.connected = 1;
								event->connection.in = 0;
								event->connection.out = 0;
								modem->helper->event(modem->helper->modem, event);
							}
							/* on_trigger_colp */
							static void _on_trigger_colp(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								char buf[32];
								unsigned int u;
								if(sscanf(answer, "\"%31[^\"]\",%u", buf, &u) != 2)
									return; /* FIXME there may be different or more information */
								buf[sizeof(buf) - 1] = '\0';
								free(hayes->call_number);
								switch(u)
								{
									case 145:
										if((hayes->call_number = malloc(sizeof(buf) + 1))
												== NULL)
											break;
										snprintf(hayes->call_number, sizeof(buf) + 1, "%s%s",
												"+", buf);
										break;
									default:
										hayes->call_number = strdup(buf);
										break;
								}
								event->call.number = hayes->call_number;
							}
							/* on_trigger_cops */
							static void _on_trigger_cops(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
								unsigned int u;
								unsigned int v;
								char buf[32];
								unsigned int w;
								if(sscanf(answer, "%u,%u,\"%31[^\"]\",%u", &u, &v, buf, &w) < 3)
									return; /* FIXME is also valid with 1 result */
								buf[sizeof(buf) - 1] = '\0';
								free(hayes->registration_operator);
								hayes->registration_operator = strdup(buf);
								event->registration._operator = hayes->registration_operator;
								switch(u)
								{
									case 0:
										u = MODEM_REGISTRATION_MODE_AUTOMATIC;
										break;
									case 1:
										u = MODEM_REGISTRATION_MODE_MANUAL;
										break;
									case 2:
										u = MODEM_REGISTRATION_MODE_DISABLED;
										break;
									case 3: /* only for setting the format */
									default:
										u = event->registration.mode;
										break;
								}
								/* this is usually worth an event */
								modem->helper->event(modem->helper->modem, event);
							}
							/* on_trigger_cpas */
							static void _on_trigger_cpas(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								unsigned int u;
								if(sscanf(answer, "%u", &u) != 1)
									return;
								switch(u)
								{
									case 0:
										event->call.status = MODEM_CALL_STATUS_NONE;
										event->call.direction = MODEM_CALL_DIRECTION_NONE;
										/* report connection status */
										event = &hayes->events[MODEM_EVENT_TYPE_CONNECTION];
										event->connection.connected = 0;
										event->connection.in = 0;
										event->connection.out = 0;
										modem->helper->event(modem->helper->modem, event);
										break;
									case 3:
										event->call.status = MODEM_CALL_STATUS_RINGING;
										/* report event */
										modem->helper->event(modem->helper->modem, event);
										break;
									case 4:
										event->call.status = MODEM_CALL_STATUS_ACTIVE;
										event->call.direction = MODEM_CALL_DIRECTION_NONE;
										break;
									case 2: /* XXX unknown */
									default:
										break;
								}
							}
							/* on_trigger_cpbr */
							static void _on_trigger_cpbr(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemRequest request;
								HayesRequestContactList list;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CONTACT];
								unsigned int u;
								unsigned int v;
								char number[32];
								char name[32];
								char * p;
								if(sscanf(answer, "(%u-%u)", &u, &v) == 2)
								{
									memset(&request, 0, sizeof(request));
									request.type = HAYES_REQUEST_CONTACT_LIST;
									list.from = u;
									list.to = v;
									request.plugin.data = &list;
									_hayes_request(modem, &request);
									return;
								}
								if(sscanf(answer, "%u,\"%31[^\"]\",%u,\"%31[^\"]\"",
											&event->contact.id, number, &u, name) != 4)
									return;
								number[sizeof(number) - 1] = '\0';
								/* FIXME the number may have to be modified */
								free(hayes->contact_number);
								hayes->contact_number = strdup(number);
								event->contact.number = hayes->contact_number;
								name[sizeof(name) - 1] = '\0';
							#if 1 /* FIXME is it really always in ISO-8859-1? */
								if((p = g_convert(name, -1, "UTF-8", "ISO-8859-1", NULL, NULL, NULL))
										                        != NULL)
								{
									snprintf(name, sizeof(name), "%s", p);
									g_free(p);
								}
							#endif
								free(hayes->contact_name);
								hayes->contact_name = strdup(name);
								event->contact.name = hayes->contact_name;
								event->contact.status = MODEM_CONTACT_STATUS_OFFLINE;
								/* send event */
								modem->helper->event(modem->helper->modem, event);
							}
							/* on_trigger_cpin */
							static void _on_trigger_cpin(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_AUTHENTICATION];
								char * p;
								if(strcmp(answer, "READY") == 0 || strcmp(answer, "OK") == 0)
									event->authentication.status = MODEM_AUTHENTICATION_STATUS_OK;
								else if(strcmp(answer, "SIM PIN") == 0
										|| strcmp(answer, "SIM PUK") == 0)
								{
									free(hayes->authentication_name);
									p = strdup(answer);
									hayes->authentication_name = p;
									event->authentication.name = p;
									event->authentication.method = MODEM_AUTHENTICATION_METHOD_PIN;
									event->authentication.status
										= MODEM_AUTHENTICATION_STATUS_REQUIRED;
									/* FIXME also provide remaining retries */
								}
							}
							/* on_trigger_creg */
							static void _on_trigger_creg(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
								int res;
								unsigned int u[4] = { 0, 0, 0, 0 };
								ModemRequest request;
								res = sscanf(answer, "%u,%u,%X,%X", &u[0], &u[1], &u[2], &u[3]);
								if(res == 0)
									return;
								if(res == 1 || res == 3)
									memmove(&u[1], u, sizeof(*u) * 3);
								u[0] = event->registration.mode;
								switch(u[1])
								{
									case 0:
										u[0] = MODEM_REGISTRATION_MODE_DISABLED;
										u[1] = MODEM_REGISTRATION_STATUS_NOT_SEARCHING;
										break;
									case 1:
										u[1] = MODEM_REGISTRATION_STATUS_REGISTERED;
										break;
									case 2:
										u[1] = MODEM_REGISTRATION_STATUS_SEARCHING;
										break;
									case 3:
										u[1] = MODEM_REGISTRATION_STATUS_DENIED;
										break;
									case 5:
										u[1] = MODEM_REGISTRATION_STATUS_REGISTERED;
										break;
									case 4: /* unknown */
									default:
										u[0] = MODEM_REGISTRATION_MODE_UNKNOWN;
										u[1] = MODEM_REGISTRATION_STATUS_UNKNOWN;
										break;
								}
								event->registration.mode = u[0];
								switch((event->registration.status = u[1]))
								{
									case MODEM_REGISTRATION_STATUS_REGISTERED:
										memset(&request, 0, sizeof(request));
										request.type = HAYES_REQUEST_GPRS_ATTACHED;
										_hayes_request(modem, &request);
										request.type = HAYES_REQUEST_OPERATOR;
										_hayes_request(modem, &request);
										request.type = HAYES_REQUEST_SIGNAL_LEVEL;
										_hayes_request(modem, &request);
										break;
									default:
										event->registration.signal = 0.0 / 0.0;
										/* this is usually an unsollicited event */
										modem->helper->event(modem->helper->modem, event);
										break;
								}
							}
							/* on_trigger_cring */
							static void _on_trigger_cring(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_CALL];
								if(strcmp(answer, "VOICE") == 0)
									; /* FIXME implement */
								event->call.status = MODEM_CALL_STATUS_RINGING;
								event->call.direction = MODEM_CALL_DIRECTION_INCOMING;
								event->call.number = "";
								/* this is always an unsollicited event */
								modem->helper->event(modem->helper->modem, event);
							}
							/* on_trigger_csq */
							static void _on_trigger_csq(ModemPlugin * modem, char const * answer)
							{
								Hayes * hayes = modem->priv;
								ModemEvent * event = &hayes->events[MODEM_EVENT_TYPE_REGISTRATION];
								unsigned int u;
								unsigned int v;
								if(sscanf(answer, "%u,%u", &u, &v) != 2)
									return;
								if(u > 31)
									event->registration.signal = 0.0 / 0.0;
								else
									event->registration.signal = (u / 32) + 0.0;
								/* this is usually worth an event */
								modem->helper->event(modem->helper->modem, event);
							}
							/* on_trigger_cusd */
							static void _on_trigger_cusd(ModemPlugin * modem, char const * answer)
							{
								unsigned int u;
								/* FIXME really implement */
								if(sscanf(answer, "%u", &u) != 1)
									return;
							}
							/* helpers */
							/* is_figure */
							static int _is_figure(int c)
							{
								if(c >= '0' && c <= '9')
									return 1;
								if(c == '*' || c == '+' || c == '#')
									return 1;
								if(c >= 'A' && c <= 'D')
									return 1;
								return 0;
							}
							/* is_number */
							static int _is_number(char const * number)
							{
								if(number == NULL || number[0] == '\0')
									return 0;
								while(*number != '\0')
									if(!_is_figure(*(number++)))
										return 0;
								return 1;
							}
							