/*
 * Copyright (c) 2007 OpenMoko, Inc.
 * Modified for use in QEMU by Andrzej Zaborowski <andrew@openedhand.com>
 */
/*

  $Id: at-emulator.c,v 1.45 2006/10/03 21:26:37 pkot Exp $

  G N O K I I

  A Linux/Unix toolset and driver for the mobile phones.

  This file is part of gnokii.

  Gnokii 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; either version 2 of the License, or
  (at your option) any later version.

  Gnokii 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 gnokii; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

  Copyright (C) 1999-2000  Hugh Blemings & Pavel Janík ml.
  Copyright (C) 2001-2004  Pawel Kot
  Copyright (C) 2002-2004  BORBELY Zoltan

  This file provides a virtual modem or "AT" interface to the GSM phone by
  calling code in gsm-api.c. Inspired by and in places copied from the Linux
  kernel AT Emulator IDSN code by Fritz Elfert and others.

*/

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <grp.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <ctype.h>
#include <termios.h>

#include "misc.h"
#include "gnokii.h"
#include "compat.h"
#include "at-emulator.h"
#include "datapump.h"

#define MAX_LINE_LENGTH 256

#define	MAX_CMD_BUFFERS	(2)
#define	CMD_BUFFER_LENGTH (100)

/* Definition of some special Registers of AT-Emulator, pinched in
   part from ISDN driver in Linux kernel */
#define REG_RINGATA   0
#define REG_RINGCNT   1
#define REG_ESC       2
#define REG_CR        3
#define REG_LF        4
#define REG_BS        5
#define	S22          22
#define S35          35
#define REG_CTRLZ   100
#define REG_ESCAPE  101

#define REG_QUIET    14
#define BIT_QUIET     4
#define REG_VERBOSE  14
#define BIT_VERBOSE   8
#define REG_ECHO     14
#define BIT_ECHO      2


#define	MAX_MODEM_REGISTERS	102

/* Message format definitions */
#define PDU_MODE      0
#define TEXT_MODE     1
#define INTERACT_MODE 2

/* Global variables */
bool gn_atem_initialised = false;	/* Set to true once initialised */
extern bool CommandMode;
extern int ConnectCount;

struct gn_statemachine *sm;
gn_data data;

static gn_sms sms;
static gn_call_info callinfo;
static 	char imei[64], model[64], revision[64], manufacturer[64];

/* Local variables */
static u8	ModemRegisters[MAX_MODEM_REGISTERS];
static char	CmdBuffer[MAX_CMD_BUFFERS][CMD_BUFFER_LENGTH];
static int	CurrentCmdBuffer;
static int	CurrentCmdBufferIndex;
static int	IncomingCallNo;
static int	MessageFormat;          /* Message Format (text or pdu) */
static int	CallerIDMode;

	/* Current command parser */
static void 	(*Parser)(char *); /* Current command parser */
/* void 	(*Parser)(char *) = gn_atem_at_parse; */

static gn_memory_type 	SMSType;
static int 	SMSNumber;

/* If initialised in debug mode, stdin/out is used instead
   of ptys for interface. */
bool gn_atem_initialise(struct gn_statemachine *vmsm)
{
	gn_data_clear(&data);
	memset(&sms, 0, sizeof(sms));
	memset(&callinfo, 0, sizeof(callinfo));

	data.sms = &sms;
	data.call_info = &callinfo;
	data.manufacturer = manufacturer;
	data.model = model;
	data.revision = revision;
	data.imei = imei;

	sm = vmsm;

	/* Initialise command buffer variables */
	CurrentCmdBuffer = 0;
	CurrentCmdBufferIndex = 0;

	/* Initialise registers */
	gn_atem_registers_init();

	/* Initial parser is AT routine */
	Parser = gn_atem_at_parse;

	/* Setup defaults for AT*C interpreter. */
	SMSNumber = 1;
	SMSType = GN_MT_ME;

	/* Default message format is PDU */
	MessageFormat = PDU_MODE;

	/* Set the call passup so that we get notified of incoming calls */
	data.call_notification = gn_atem_call_passup;
	gn_sm_functions(GN_OP_SetCallNotification, &data, sm);

	/* query model, revision and imei */
	if (gn_sm_functions(GN_OP_Identify, &data, sm) != GN_ERR_NONE)
		return false;

	/* We're ready to roll... */
	gn_atem_initialised = true;
	data.connected = 0;
	return (true);
}

/* Initialise the "registers" used by the virtual modem. */
void	gn_atem_registers_init(void)
{
	memset(ModemRegisters, 0, sizeof(ModemRegisters));

	ModemRegisters[REG_RINGATA] = 0;
	ModemRegisters[REG_RINGCNT] = 0;
	ModemRegisters[REG_ESC] = '+';
	ModemRegisters[REG_CR] = 13;
	ModemRegisters[REG_LF] = 10;
	ModemRegisters[REG_BS] = 8;
	ModemRegisters[S35] = 7;
	ModemRegisters[REG_ECHO] |= BIT_ECHO;
	ModemRegisters[REG_VERBOSE] |= BIT_VERBOSE;
	ModemRegisters[REG_CTRLZ] = 26;
	ModemRegisters[REG_ESCAPE] = 27;

	CallerIDMode = 0;
}


static void  gn_atem_hangup_phone(void)
{
	if (IncomingCallNo > 0) {
		rlp_user_request_set(Disc_Req, true);
		gn_sm_loop(10, sm);
	}
	if (IncomingCallNo > 0) {
		data.call_info->call_id = IncomingCallNo;
		gn_sm_functions(GN_OP_CancelCall, &data, sm);
		IncomingCallNo = -1;
	}
	dp_Initialise();
}


static void  gn_atem_answer_phone(void)
{
	/* For now we'll also initialise the datapump + rlp code again */
	dp_Initialise();
	data.call_notification = dp_CallPassup;
	gn_sm_functions(GN_OP_SetCallNotification, &data, sm);
	data.call_info->call_id = IncomingCallNo;
	gn_sm_functions(GN_OP_AnswerCall, &data, sm);
	CommandMode = false;
}


/* This gets called to indicate an incoming call */
void gn_atem_call_passup(gn_call_status CallStatus, gn_call_info *CallInfo, struct gn_statemachine *state)
{
	dprintf("gn_atem_call_passup called with %d\n", CallStatus);

	switch (CallStatus) {
	case GN_CALL_Incoming:
		IncomingCallNo = CallInfo->call_id;
		gn_atem_modem_result(MR_RING);
		ModemRegisters[REG_RINGCNT]++;
		gn_atem_cid_out(CallInfo);
		if (ModemRegisters[REG_RINGATA] != 0) gn_atem_answer_phone();
		break;
	case GN_CALL_LocalHangup:
	case GN_CALL_RemoteHangup:
		if (IncomingCallNo > 0) {
			gn_atem_cpi(GSMD_CALLPROG_DISCONNECT,
					GSMD_CALL_DIR_MT, 0);
			gn_atem_cpi(GSMD_CALLPROG_RELEASE,
					GSMD_CALL_DIR_MT, 0);
		}
		IncomingCallNo = -1;
		break;
	default:
		break;
	}
}

/* This gets called to output caller id info of incoming call */
void gn_atem_cid_out(gn_call_info *CallInfo)
{
	struct tm *now;
	time_t nowh;
	char buf[14]; /* 7 for "DATE = " + 4 digits + \n + \r + \0 */

	nowh = time(NULL);
	now = localtime(&nowh);

	switch (CallerIDMode) {
	case 0:	/* no output */
		break;
	case 1: /* formatted CID */
		snprintf(buf, sizeof(buf), "DATE = %02d%02d\r\n", now->tm_mon + 1, now->tm_mday);
		gn_atem_string_out(buf);

		snprintf(buf, sizeof(buf), "TIME = %02d%02d\r\n", now->tm_hour, now->tm_min);
		gn_atem_string_out(buf);

		/* TO DO: handle P and O numbers */
		gn_atem_string_out("NMBR = ");
		gn_atem_string_out(1 + CallInfo->number); /* skip leading "+" */
		gn_atem_string_out("\r\nNAME = ");
		gn_atem_string_out(CallInfo->name);
		gn_atem_string_out("\r\n");

		/* FIX ME: do a real emulation of rings after the first one (at a lower level than this) */
		gn_atem_modem_result(MR_RING);

		break;

	}
}

/* Handler called when characters received from serial port.
   calls state machine code to process it. */
void	gn_atem_incoming_data_handle(const char *buffer, int length)
{
	int count;
	unsigned char out_buf[3];

	for (count = 0; count < length ; count++) {

		/* If it's a command terminator character, parse what
		   we have so far then go to next buffer. */
		if (buffer[count] == ModemRegisters[REG_CR] ||
		    buffer[count] == ModemRegisters[REG_LF] ||
		    buffer[count] == ModemRegisters[REG_CTRLZ] ||
		    buffer[count] == ModemRegisters[REG_ESCAPE]) {

			/* Echo character if appropriate. */
			if (buffer[count] == ModemRegisters[REG_CR] &&
				(ModemRegisters[REG_ECHO] & BIT_ECHO)) {
				gn_atem_string_out("\r");	/* XXX */
			}

			/* Save CTRL-Z and ESCAPE for the parser */
			if (buffer[count] == ModemRegisters[REG_CTRLZ] ||
			    buffer[count] == ModemRegisters[REG_ESCAPE])
				CmdBuffer[CurrentCmdBuffer][CurrentCmdBufferIndex++] = buffer[count];

			CmdBuffer[CurrentCmdBuffer][CurrentCmdBufferIndex] = 0x00;

			Parser(CmdBuffer[CurrentCmdBuffer]);

			CurrentCmdBuffer++;
			if (CurrentCmdBuffer >= MAX_CMD_BUFFERS) {
				CurrentCmdBuffer = 0;
			}
			CurrentCmdBufferIndex = 0;

		} else if (buffer[count] == ModemRegisters[REG_BS]) {
			if (CurrentCmdBufferIndex > 0) {
				/* Echo character if appropriate. */
				if (ModemRegisters[REG_ECHO] & BIT_ECHO) {
					gn_atem_string_out("\b \b");
				}

				CurrentCmdBufferIndex--;
			}
		} else {
			/* Echo character if appropriate. */
			if (ModemRegisters[REG_ECHO] & BIT_ECHO) {
				out_buf[0] = buffer[count];
				out_buf[1] = 0;
				gn_atem_string_out((char *)out_buf);
			}

			/* Collect it to command buffer */
			CmdBuffer[CurrentCmdBuffer][CurrentCmdBufferIndex++] = buffer[count];
			if (CurrentCmdBufferIndex >= CMD_BUFFER_LENGTH) {
				CurrentCmdBufferIndex = CMD_BUFFER_LENGTH;
			}
		}
	}
}

static char *gn_atem_cme(int code)
{
	static char err[80];

	if (!strcmp(data.cmee, "0"))
		return "ERROR";

	if (!strcmp(data.cmee, "1"))
		snprintf(err, sizeof(err), "+CME ERROR: %i", code);

	if (!strcmp(data.cmee, "2"))
		snprintf(err, sizeof(err), "+CME ERROR: phone error");

	return err;
}

struct gn_atem_op {
	char *op;
	int writable;
	enum {
		gn_var_string,	/* "A","B","C" */
		gn_var_bool,	/* 0,1 */
		gn_var_numbers,	/* (1-5),(9-20) */
	} type;
	bool (*set_val)(char **buf, struct gn_atem_op *op, char *val);
	char *default_val;
	char *string_val[];
};

bool	gn_atem_parse_option(char **buf, struct gn_atem_op *op, char *val)
{
	char	buffer[MAX_LINE_LENGTH], **strval;
	int	len;

	if (*val == 0)
		strcpy(val, op->default_val);

	if ((*buf)[0] == 0 || ((*buf)[0] == '?' && (*buf)[1] == 0)) {
		*buf += strlen(*buf);
		gsprintf(buffer, MAX_LINE_LENGTH, "%s: %s\r\n", op->op, val);
		gn_atem_string_out(buffer);
		return (false);
	}

	if (*(*buf) ++ != '=')
		return (true);
	if (!strcasecmp(*buf, "?")) {
		(*buf) ++;
		len = gsprintf(buffer, MAX_LINE_LENGTH, "%s: ", op->op);
		switch (op->type) {
		case gn_var_string:
			strval = op->string_val;
			len += gsprintf(buffer + len,
					MAX_LINE_LENGTH - len,
					"\"%s\"", *strval++);
			while (*strval)
				len += gsprintf(buffer + len,
						MAX_LINE_LENGTH - len,
						",\"%s\"", *strval++);
			break;

		case gn_var_numbers:
			strval = op->string_val;
			len += gsprintf(buffer + len,
					MAX_LINE_LENGTH - len,
					"\"%s\"", *strval++);
			/* TODO */
			break;

		case gn_var_bool:
			len += gsprintf(buffer + len,
					MAX_LINE_LENGTH - len, "(0,1)");
			break;
		}
		gsprintf(buffer + len, MAX_LINE_LENGTH - len, "\r\n");
		return (false);
	}

	if (!op->writable)
		return (true);

	if (op->set_val)
		return op->set_val(buf, op, val);

	switch (op->type) {
	case gn_var_string:
		for (strval = op->string_val; *strval; strval++)
			if (!strcasecmp(*buf, *strval)) {
				gsprintf(val, MAX_LINE_LENGTH,
						"\"%s\"", *strval);
				*buf += strlen(*buf);
				return (false);
			}
		break;

	case gn_var_bool:
		switch (gn_atem_num_get(buf)) {
		case 0:
			strcpy(val, "0");
			return (false);
		case 1:
			strcpy(val, "1");
			return (false);
		}
		break;

	default:
		break;
	}

	return (true);
}

static struct gn_atem_op gn_atem_op_cscs = {
	.op		= "+CSCS",
	.writable	= 1,
	.type		= gn_var_string,
	.default_val	= "IRA",
	.string_val	= {
		"GSM", "IRA", "PCCP437", "PCDN", "8859-1", "HEX", "UCS2", 0,
	},
};

static struct gn_atem_op gn_atem_op_cmux = {
	.op		= "+CMUX",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "1,0,1,10,1,0,2,1,1",
	.string_val	= {
		"(1),(0),(1-5),(10-100),(1-255),(0-100),(2-255),(1-255),(1-7)",
	},
};

static bool gn_atem_cmee_set(char **buf, struct gn_atem_op *op, char *val)
{
	switch (gn_atem_num_get(buf)) {
	case 0:
		strcpy(val, "0");
		return (false);
	case 1:
		strcpy(val, "1");
		return (false);
	case 2:
		strcpy(val, "2");
		return (false);
	}
	return (true);
}

static struct gn_atem_op gn_atem_op_cmee = {
	.op		= "+CMEE",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0",
	.string_val	= {
		"(0-2)",
	},
	.set_val	= gn_atem_cmee_set,
};

static bool gn_atem_clip_set(char **buf, struct gn_atem_op *op, char *val)
{
	switch (gn_atem_num_get(buf)) {
	case 0:
		strcpy(val, "0,1");	/* CLIP provisioned in this network */
		return (false);
	case 1:
		strcpy(val, "1,1");	/* CLIP provisioned in this network */
		return (false);
	}
	return (true);
}

static struct gn_atem_op gn_atem_op_clip = {
	.op		= "+CLIP",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0,1",
	.string_val	= {
		"(0,1)",
	},
	.set_val	= gn_atem_clip_set,
};

static bool gn_atem_colp_set(char **buf, struct gn_atem_op *op, char *val)
{
	switch (gn_atem_num_get(buf)) {
	case 0:
		strcpy(val, "0,1");	/* COLP provisioned in this network */
		return (false);
	case 1:
		strcpy(val, "1,1");	/* COLP provisioned in this network */
		return (false);
	}
	return (true);
}

static struct gn_atem_op gn_atem_op_colp = {
	.op		= "+COLP",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0,1",
	.string_val	= {
		"(0,1)",
	},
	.set_val	= gn_atem_colp_set,
};

static struct gn_atem_op gn_atem_op_ws46 = {
	.op		= "+WS46",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "12",
	.string_val	= {
		"(12)",
	},
};

static struct gn_atem_op gn_atem_op_csta = {
	.op		= "+CSTA",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "129",
	.string_val	= {
		"(129,145)",
	},
};

static struct gn_atem_op gn_atem_op_cmod = {
	.op		= "+CMOD",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0",
	.string_val	= {
		"(0-3)",
	},
};

static struct gn_atem_op gn_atem_op_cbst = {
	.op		= "+CBST",
	.writable	= 0,
	.type		= gn_var_numbers,
	.default_val	= "7,0,1",
	.string_val	= {
		"(0-7,12,14,65,66,68,70,71,75),(0),(0-3)",
	},
};

static struct gn_atem_op gn_atem_op_crlp = {
	.op		= "+CRLP",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "61,61,48,6",
	.string_val	= {
		"(0-61),(0-61),(39-255),(1-255)",
	},
};

static struct gn_atem_op gn_atem_op_cr = {
	.op		= "+CR",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0",
	.string_val	= {
		"(0,1)",
	},
};

static struct gn_atem_op gn_atem_op_crc = {
	.op		= "+CRC",
	.writable	= 1,
	.type		= gn_var_bool,
	.default_val	= "0",
};

static struct gn_atem_op gn_atem_op_csns = {
	.op		= "+CSNS",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0",
	.string_val	= {
		"(0-7)",
	},
};

static bool gn_atem_creg_set(char **buf, struct gn_atem_op *op, char *val)
{
	/* <stat> values are:
	 * 0	not registered, MT is not currently searching a
	 *	new operator to register to
	 * 1	registered, home network
	 * 2	not registered, but MT is currently searching a
	 *	new operator to register to
	 * 3	registration denied
	 * 4	unknown
	 * 5	registered, roaming
	 */
	switch (gn_atem_num_get(buf)) {
	case 0:
		/* No unsolicited +CREG. */
		strcpy(val, "0,1");
		return (false);
	case 1:
		/* +CREG Syntax is <stat> when there's a change in network
		 * registration status.
		 */
		strcpy(val, "1,1");
		return (false);
	case 2:
		/* +CREG Syntax is <stat>[,<lac>,<ci>[,<AcT>]] when we've (MT)
		 * succesfully registered in the network cell.
		 */
		strcpy(val, "2,1");
		return (false);
	}
	return (true);
}

static struct gn_atem_op gn_atem_op_creg = {
	.op		= "+CREG",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0,0",
	.string_val	= {
		"(0-2)",
	},
	.set_val	= gn_atem_creg_set,
};

static void gn_atem_network_msg(int connected,
			struct gn_statemachine *state)
{
	char *buffer;

	data.connected = connected;
	/* TODO: Send +COPS and +CREG separately */
	/* TODO: Take COPS format and CREG format settings into account */
	if (connected) {
		strcpy(data.cops, "1,\"012C\",\"0DCC\"");
	} else {
		strcpy(data.cops, "2");
	}
	if (strcmp(data.creg, "0"))
		asprintf(&buffer, "+CREG: %s\r\n", data.cops);
	else
		/* +CREG: is disabled, but +COPS is allowed */
		asprintf(&buffer, "+COPS: %s\r\n", data.cops);
	gn_atem_string_out(buffer);
	free(buffer);
}

static bool gn_atem_cops_set(char **buf, struct gn_atem_op *op, char *val)
{
	data.network_change_notification = gn_atem_network_msg;

	/* Syntax is <mode>[,<format>[,<oper>]] */
	switch (gn_atem_num_get(buf)) {	/* Parse <mode> */
	case 0:
	case 1:
	case 4:
		if (gn_sm_functions(GN_OP_NetworkRegister, &data, sm) !=
				GN_ERR_NONE)
			break;
		return (false);
	case 2:
		if (gn_sm_functions(GN_OP_NetworkUnregister, &data, sm) !=
				GN_ERR_NONE)
			break;
		return (false);
	case 3:	/* Only sets <format>, TODO */
		return (false);
	}
	return (true);
}

static struct gn_atem_op gn_atem_op_cops = {
	.op		= "+COPS",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "2",
	.string_val	= {
		"(0-4)",
	},
	.set_val	= gn_atem_cops_set,
};

static struct gn_atem_op gn_atem_op_cpas = {
	.op		= "+CPAS",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0",
	.string_val	= {
		"(0-5)",
	},
};

static bool gn_atem_cfun_set(char **buf, struct gn_atem_op *op, char *val)
{
	/* Format is <fun>[,<Rst] */
	switch (gn_atem_num_get(buf)) {	/* Ignore the Reset argument */
	case 0:
		/* Minimum functionality */
		strcpy(val, "0");
		return (false);
	case 1:
		/* Full functionality */
		strcpy(val, "1");
		return (false);
	case 2:
		/* Disable phone transmit RF circuits only */
		strcpy(val, "2");
		return (false);
	case 3:
		/* Disable phone receive RF circuits only */
		strcpy(val, "3");
		return (false);
	case 4:
		/* Disable phone both transmit and receive RF circuits */
		strcpy(val, "4");
		return (false);
	}
	return (true);
}

static struct gn_atem_op gn_atem_op_cfun = {
	.op		= "+CFUN",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "1",
	.string_val	= {
		"(0-4),(0)",
	},
	.set_val	= gn_atem_cfun_set,
};

static struct gn_atem_op gn_atem_op_ctzr = {
	.op		= "+CTZR",
	.writable	= 1,
	.type		= gn_var_bool,
	.default_val	= "0",
};

static struct gn_atem_op gn_atem_op_cbc = {
	.op		= "+CBC",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0,0",
	.string_val	= {
		"(0-3),(0-100)",
	},
};

static bool gn_atem_band_set(char **buf, struct gn_atem_op *op, char *val)
{
	/* Syntax is <band>[,<mode>] */
	switch (gn_atem_num_get(buf)) {
	case 0:
		/* Automatic */
		strcpy(val, "0");
		break;
	case 1:
		/* Manual */
		strcpy(val, "1");
		break;
	default:
		return (true);
	}
	return (false);
}

static struct gn_atem_op gn_atem_op_band = {
	.op		= "%BAND",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0",
	.string_val	= {
		"(0-1),(1-31)",
	},
	.set_val	= gn_atem_band_set,
};

static bool gn_atem_cpi_set(char **buf, struct gn_atem_op *op, char *val)
{
	switch (gn_atem_num_get(buf)) {
	case 0:
		/* Disable */
		strcpy(val, "0");
		break;
	case 1:
		/* Enable */
		strcpy(val, "1");
		break;
	case 2:
		/* Status */
		strcpy(val, "2");
		break;
	case 3:
		/* Append cause and ALS bearer state to unsolicited results */
		strcpy(val, "3");
		break;
	case 4:
		/* Append Advance Cause Code */
		strcpy(val, "4");
		break;
	default:
		return (true);
	}
	return (false);
}

void	gn_atem_cpi(enum gsmd_call_progress msg,
		enum gsmd_call_direction dir, int inband)
{
	char *buffer;

	if (data.cpi[0] < '1')
		return;

	/* Format: %CPI: <cId>,<msgType>,<ibt>,<tch>,<dir>,[<mode>],
	 * [<number>],[<type>],[<alpha>],[<cause>],<line> */
	asprintf(&buffer, "%%CPI: %i,%i,0,%i,%i,0,,,,,0\r\n",
			IncomingCallNo, msg, inband, dir);
	gn_atem_string_out(buffer);
	free(buffer);
}

static struct gn_atem_op gn_atem_op_cpi = {
	.op		= "%CPI",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "1",
	.string_val	= {
		"(0-4)",
	},
	.set_val	= gn_atem_cpi_set,
};

static struct gn_atem_op gn_atem_op_cssn = {
	.op		= "+CSSN",
	.writable	= 1,
	.type		= gn_var_numbers,
	.default_val	= "0,0",
	.string_val	= {
		"(0,1),(0,1)",
	},
};

/* Parser for standard AT commands.  cmd_buffer must be null terminated. */
void	gn_atem_at_parse(char *cmd_buffer)
{
	char *buf;
	int regno, val;
	char str[256];

	if (!cmd_buffer[0])
		return;

	if (strncasecmp(cmd_buffer, "AT", 2) != 0) {
		gn_atem_modem_result(sm->info->non_at_ok ? MR_OK : MR_ERROR);
		return;
	}

	for (buf = &cmd_buffer[2]; *buf;) {
		switch (toupper(*buf)) {

		case 'Z':
			/* Reset modem */
			buf++;
			switch (gn_atem_num_get(&buf)) {
			case -1:
			case 0:	/* reset and load stored profile 0 */
			case 1:	/* reset and load stored profile 1 */
				gn_atem_hangup_phone();
				gn_atem_registers_init();
				break;
			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		case 'A':
		        /* Answer call */
			buf++;
			gn_atem_answer_phone();
			return;

		case 'D':
			/* Dial Data :-) */
			/* FIXME - should parse this better */
			/* For now we'll also initialise the datapump + rlp code again */
			dp_Initialise();
			buf++;
			if (toupper(*buf) == 'T' || toupper(*buf) == 'P') buf++;
			while (*buf == ' ') buf++;
			data.call_notification = dp_CallPassup;
			gn_sm_functions(GN_OP_SetCallNotification, &data, sm);
			snprintf(data.call_info->number, sizeof(data.call_info->number), "%s", buf);
			if (ModemRegisters[S35] == 0)
				data.call_info->type = GN_CALL_DigitalData;
			else
				data.call_info->type = GN_CALL_NonDigitalData;
			data.call_info->send_number = GN_CALL_Default;
			CommandMode = false;
			if (gn_sm_functions(GN_OP_MakeCall, &data, sm) != GN_ERR_NONE) {
				CommandMode = true;
				dp_CallPassup(GN_CALL_RemoteHangup, NULL, NULL);
				data.call_notification = gn_atem_call_passup;
				gn_sm_functions(GN_OP_SetCallNotification, &data, sm);
			} else {
				IncomingCallNo = data.call_info->call_id;
				gn_sm_loop(10, sm);
			}
			return;

		case 'H':
			/* Hang Up */
			buf++;
			switch (gn_atem_num_get(&buf)) {
			case -1:
			case 0:	/* hook off the phone */
				if (IncomingCallNo > 0) {
					gn_atem_cpi(GSMD_CALLPROG_DISCONNECT,
							GSMD_CALL_DIR_MT, 0);
					gn_atem_cpi(GSMD_CALLPROG_RELEASE,
							GSMD_CALL_DIR_MT, 0);
				}
				gn_atem_hangup_phone();
				break;
			case 1:	/* hook on the phone */
				break;
			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		case 'S':
			/* Change registers */
			buf++;
			regno = gn_atem_num_get(&buf);
			if (regno < 0 || regno >= MAX_MODEM_REGISTERS) {
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			if (*buf == '=') {
				buf++;
				val = gn_atem_num_get(&buf);
				if (val < 0 || val > 255) {
					gn_atem_modem_result(MR_ERROR);
					return;
				}
				ModemRegisters[regno] = val;
			} else if (*buf == '?') {
				buf++;
				snprintf(str, sizeof(str), "%d\r\n", ModemRegisters[regno]);
				gn_atem_string_out(str);
			} else {
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		case 'E':
		        /* E - Turn Echo on/off */
			buf++;
			switch (gn_atem_num_get(&buf)) {
			case -1:
			case 0:
				ModemRegisters[REG_ECHO] &= ~BIT_ECHO;
				break;
			case 1:
				ModemRegisters[REG_ECHO] |= BIT_ECHO;
				break;
			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		case 'Q':
		        /* Q - Turn Quiet on/off */
			buf++;
			switch (gn_atem_num_get(&buf)) {
			case -1:
			case 0:
				ModemRegisters[REG_QUIET] &= ~BIT_QUIET;
				break;
			case 1:
				ModemRegisters[REG_QUIET] |= BIT_QUIET;
				break;
			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		case 'V':
		        /* V - Turn Verbose on/off */
			buf++;
			switch (gn_atem_num_get(&buf)) {
			case -1:
			case 0:
				ModemRegisters[REG_VERBOSE] &= ~BIT_VERBOSE;
				break;
			case 1:
				ModemRegisters[REG_VERBOSE] |= BIT_VERBOSE;
				break;
			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		case 'X':
		        /* X - Set verbosity of the result messages */
			buf++;
			switch (gn_atem_num_get(&buf)) {
			case -1:
			case 0: val = 0x00; break;
			case 1: val = 0x40; break;
			case 2: val = 0x50; break;
			case 3: val = 0x60; break;
			case 4: val = 0x70; break;
			case 5: val = 0x10; break;
			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			ModemRegisters[S22] = (ModemRegisters[S22] & 0x8f) | val;
			break;

		case 'I':
		        /* I - info */
			buf++;
			switch (gn_atem_num_get(&buf)) {
			case -1:
			case 0:	/* terminal id */
				snprintf(str, sizeof(str), "%d\r\n", ModemRegisters[39]);
				gn_atem_string_out(str);
				break;
			case 1:	/* serial number (IMEI) */
				snprintf(str, sizeof(str), "%s\r\n", imei);
				gn_atem_string_out(str);
				break;
			case 2: /* phone revision */
				snprintf(str, sizeof(str), "%s\r\n", revision);
				gn_atem_string_out(str);
				break;
			case 3: /* modem revision */
				gn_atem_string_out(VERSION "\r\n");
				break;
			case 4: /* OEM string */
				snprintf(str, sizeof(str), "%s %s\r\n", manufacturer, model);
				gn_atem_string_out(str);
				break;
			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		/* Handle AT* commands (Nokia proprietary I think) */
		case '*':
			buf++;
			if (!strcasecmp(buf, "NOKIATEST")) {
				gn_atem_modem_result(MR_OK); /* FIXME? */
				return;
			} else {
				if (!strcasecmp(buf, "C")) {
					gn_atem_modem_result(MR_OK);
					Parser = gn_atem_sms_parse;
					return;
				}
			}
			break;

		/* + is the precursor to another set of commands */
		case '+':
			buf++;

			/* AT+WS46 is wireless network selection */
			if (strncasecmp(buf, "WS46", 3) == 0) {
				buf += 4;
				if (!gn_atem_parse_option(&buf,
						&gn_atem_op_ws46, data.ws46))
					break;
			}

			switch (toupper(*buf)) {
			case 'C':
				buf++;
				/* Returns true if error occured */
				if (gn_atem_command_plusc(&buf) == true) {
					gn_atem_modem_result(MR_ERROR);
					return;
				}
				break;

			case 'G':
				buf++;
				/* Returns true if error occured */
				if (gn_atem_command_plusg(&buf) == true) {
					gn_atem_modem_result(MR_ERROR);
					return;
				}
				break;

			default:
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		/* # is the precursor to another set of commands */
		case '#':
			buf++;
			/* Returns true if error occured */
			if (gn_atem_command_diesis(&buf) == true) {
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		/* % is the precursor to another set of commands */
		case '%':
			buf++;
			/* Returns true if error occured */
			if (gn_atem_command_percent(&buf) == true) {
				gn_atem_modem_result(MR_ERROR);
				return;
			}
			break;

		default:
			gn_atem_modem_result(MR_ERROR);
			return;
		}
	}

	gn_atem_modem_result(MR_OK);
}


static void gn_atem_sms_print(char *line, gn_sms *message, int mode)
{
	switch (mode) {
	case INTERACT_MODE:
		gsprintf(line, MAX_LINE_LENGTH,
			_("\r\nDate/time: %d/%d/%d %d:%02d:%02d Sender: %s Msg Center: %s\r\nText: %s\r\n"),
			message->smsc_time.day, message->smsc_time.month, message->smsc_time.year,
			message->smsc_time.hour, message->smsc_time.minute, message->smsc_time.second,
			message->remote.number, message->smsc.number, message->user_data[0].u.text);
		break;
	case TEXT_MODE:
		if ((message->dcs.type == GN_SMS_DCS_GeneralDataCoding) &&
		    (message->dcs.u.general.alphabet == GN_SMS_DCS_8bit))
			gsprintf(line, MAX_LINE_LENGTH,
				_("\"%s\",\"%s\",,\"%02d/%02d/%02d,%02d:%02d:%02d+%02d\"\r\n%s"),
				(message->status ? _("REC READ") : _("REC UNREAD")),
				message->remote.number,
				message->smsc_time.year, message->smsc_time.month, message->smsc_time.day,
				message->smsc_time.hour, message->smsc_time.minute, message->smsc_time.second,
				message->time.timezone, _("<Not implemented>"));
		else
			gsprintf(line, MAX_LINE_LENGTH,
				_("\"%s\",\"%s\",,\"%02d/%02d/%02d,%02d:%02d:%02d+%02d\"\r\n%s"),
				(message->status ? _("REC READ") : _("REC UNREAD")),
				message->remote.number,
				message->smsc_time.year, message->smsc_time.month, message->smsc_time.day,
				message->smsc_time.hour, message->smsc_time.minute, message->smsc_time.second,
				message->time.timezone, message->user_data[0].u.text);
		break;
	case PDU_MODE:
		gsprintf(line, MAX_LINE_LENGTH, _("0,<Not implemented>"));
		break;
	default:
		gsprintf(line, MAX_LINE_LENGTH, _("<Unknown mode>"));
		break;
	}
}


static void gn_atem_sms_handle()
{
	gn_error	error;
	char		buffer[MAX_LINE_LENGTH];

	data.sms->memory_type = SMSType;
	data.sms->number = SMSNumber;
	error = gn_sms_get(&data, sm);

	switch (error) {
	case GN_ERR_NONE:
		gn_atem_sms_print(buffer, data.sms, INTERACT_MODE);
		gn_atem_string_out(buffer);
		break;
	default:
		gsprintf(buffer, MAX_LINE_LENGTH, _("\r\nNo message under number %d\r\n"), SMSNumber);
		gn_atem_string_out(buffer);
		break;
	}
	return;
}

/* Parser for SMS interactive mode */
void	gn_atem_sms_parse(char *buff)
{
	if (!strcasecmp(buff, "HELP")) {
		gn_atem_string_out(_("\r\nThe following commands work...\r\n"));
		gn_atem_string_out("DIR\r\n");
		gn_atem_string_out("EXIT\r\n");
		gn_atem_string_out("HELP\r\n");
		return;
	}

	if (!strcasecmp(buff, "DIR")) {
		SMSNumber = 1;
		gn_atem_sms_handle();
		Parser = gn_atem_dir_parse;
		return;
	}
	if (!strcasecmp(buff, "EXIT")) {
		Parser = gn_atem_at_parse;
		gn_atem_modem_result(MR_OK);
		return;
	}
	gn_atem_modem_result(MR_ERROR);
}

/* Parser for DIR sub mode of SMS interactive mode. */
void	gn_atem_dir_parse(char *buff)
{
	switch (toupper(*buff)) {
		case 'P':
			SMSNumber--;
			gn_atem_sms_handle();
			return;
		case 'N':
			SMSNumber++;
			gn_atem_sms_handle();
			return;
		case 'D':
			data.sms->memory_type = SMSType;
			data.sms->number = SMSNumber;
			if (gn_sm_functions(GN_OP_DeleteSMS, &data, sm) == GN_ERR_NONE) {
				gn_atem_modem_result(MR_OK);
			} else {
				gn_atem_string_out(gn_atem_cme(21));
				gn_atem_string_out("\r\n");
			}
			return;
		case 'Q':
			Parser= gn_atem_sms_parse;
			gn_atem_modem_result(MR_OK);
			return;
	}
	gn_atem_modem_result(MR_ERROR);
}

/* Parser for entering message content (+CMGS) */
void	gn_atem_sms_parseText(char *buff)
{
	static int index = 0;
	int i, length;
	char buffer[MAX_LINE_LENGTH];
	gn_error error;

	length = strlen(buff);

	sms.user_data[0].type = GN_SMS_DATA_Text;

	for (i = 0; i < length; i++) {

		if (buff[i] == ModemRegisters[REG_CTRLZ]) {
			/* Exit SMS text mode with sending */
			sms.user_data[0].u.text[index] = 0;
			sms.user_data[0].length = index;
			index = 0;
			Parser = gn_atem_at_parse;
			dprintf("Sending SMS to %s (text: %s)\n", data.sms->remote.number, data.sms->user_data[0].u.text);

			/* FIXME: set more SMS fields before sending */
			error = gn_sms_send(&data, sm);

			if (error == GN_ERR_NONE) {
				gsprintf(buffer, MAX_LINE_LENGTH, "+CMGS: %d\r\n", data.sms->number);
				gn_atem_string_out(buffer);
				gn_atem_modem_result(MR_OK);
			} else {
				gn_atem_string_out(gn_atem_cme(0));
				gn_atem_string_out("\r\n");
			}
			return;
		} else if (buff[i] == ModemRegisters[REG_ESCAPE]) {
			/* Exit SMS text mode without sending */
			sms.user_data[0].u.text[index] = 0;
			sms.user_data[0].length = index;
			index = 0;
			Parser = gn_atem_at_parse;
			gn_atem_modem_result(MR_OK);
			return;
		} else {
			/* Appent next char to message text */
			sms.user_data[0].u.text[index++] = buff[i];
		}
	}

	/* reached the end of line so insert \n and wait for more */
	sms.user_data[0].u.text[index++] = '\n';
	gn_atem_string_out("\r\n> ");
}

/* Handle AT+C commands, this is a quick hack together at this
   stage. */
bool	gn_atem_command_plusc(char **buf)
{
	float		rflevel = -1;
	gn_rf_unit	rfunits = GN_RF_CSQ;
	char		buffer[MAX_LINE_LENGTH], buffer2[MAX_LINE_LENGTH];
	int		status, index;
	gn_error	error;

	if (strncasecmp(*buf, "SQ", 2) == 0) {
		buf[0] += 2;

		data.rf_unit = &rfunits;
		data.rf_level = &rflevel;
		if (gn_sm_functions(GN_OP_GetRFLevel, &data, sm) == GN_ERR_NONE) {
			gsprintf(buffer, MAX_LINE_LENGTH,
					"+CSQ: %0.0f, 99\r\n",
					*(data.rf_level));
			gn_atem_string_out(buffer);
			return (false);
		} else {
			return (true);
		}
	}

	/* AT+CGMI is Manufacturer information for the ME (phone) so
	   it should be Nokia rather than gnokii... */
	if (strncasecmp(*buf, "GMI", 3) == 0) {
		buf[0] += 3;
		gsprintf(buffer, MAX_LINE_LENGTH, "%s\r\n", data.manufacturer);
		gn_atem_string_out(buffer);
		return (false);
	}

	/* AT+CGSN is IMEI */
	if (strncasecmp(*buf, "GSN", 3) == 0) {
		buf[0] += 3;
		strcpy(data.imei, gn_atem_cme(0));
		if (gn_sm_functions(GN_OP_GetImei, &data, sm) == GN_ERR_NONE) {
			gsprintf(buffer, MAX_LINE_LENGTH, "%s\r\n", data.imei);
			gn_atem_string_out(buffer);
			return (false);
		} else {
			return (true);
		}
	}

	/* AT+CGMR is Revision (hardware) */
	if (strncasecmp(*buf, "GMR", 3) == 0) {
		buf[0] += 3;
		strcpy(data.revision, gn_atem_cme(0));
		if (gn_sm_functions(GN_OP_GetRevision, &data, sm) == GN_ERR_NONE) {
			gsprintf(buffer, MAX_LINE_LENGTH, "%s\r\n", data.revision);
			gn_atem_string_out(buffer);
			return (false);
		} else {
			return (true);
		}
	}

	/* AT+CGMM is Model code  */
	if (strncasecmp(*buf, "GMM", 3) == 0) {
		buf[0] += 3;
		strcpy(data.model, gn_atem_cme(0));
		if (gn_sm_functions(GN_OP_GetModel, &data, sm) == GN_ERR_NONE) {
			gsprintf(buffer, MAX_LINE_LENGTH, "%s\r\n", data.model);
			gn_atem_string_out(buffer);
			return (false);
		} else {
			return (true);
		}
	}

	/* AT+CMGD is deleting a message */
	if (strncasecmp(*buf, "MGD", 3) == 0) {
		buf[0] += 3;
		switch (**buf) {
		case '=':
			buf[0]++;
			index = atoi(*buf);
			buf[0] += strlen(*buf);

			data.sms->memory_type = SMSType;
			data.sms->number = index;
			error = gn_sm_functions(GN_OP_DeleteSMS, &data, sm);

			switch (error) {
			case GN_ERR_NONE:
				break;
			default:
				gsprintf(buffer, MAX_LINE_LENGTH, "\r\n+CMS ERROR: %d\r\n", error);
				gn_atem_string_out(buffer);
				return (true);
			}
			break;
		default:
			return (true);
		}
		return (false);
	}

	/* AT+CMGF is mode selection for message format  */
	if (strncasecmp(*buf, "MGF", 3) == 0) {
		buf[0] += 3;
		switch (**buf) {
		case '=':
			buf[0]++;
			switch (**buf) {
			case '0':
				buf[0]++;
				MessageFormat = PDU_MODE;
				break;
			case '1':
				buf[0]++;
				MessageFormat = TEXT_MODE;
				break;
			default:
				return (true);
			}
			break;
		case '?':
			buf[0]++;
			gsprintf(buffer, MAX_LINE_LENGTH, "+CMGF: %d\r\n", MessageFormat);
			gn_atem_string_out(buffer);
			break;
		default:
			return (true);
		}
		return (false);
	}

	/* AT+CMGR is reading a message */
	if (strncasecmp(*buf, "MGR", 3) == 0) {
		buf[0] += 3;
		switch (**buf) {
		case '=':
			buf[0]++;
			index = atoi(*buf);
			buf[0] += strlen(*buf);

			data.sms->memory_type = SMSType;
			data.sms->number = index;
			error = gn_sms_get(&data, sm);

			switch (error) {
			case GN_ERR_NONE:
				gn_atem_sms_print(buffer2, data.sms, MessageFormat);
				gsprintf(buffer, MAX_LINE_LENGTH, "+CMGR: %s\r\n", buffer2);
				gn_atem_string_out(buffer);
				break;
			default:
				gsprintf(buffer, MAX_LINE_LENGTH, "\r\n+CMS ERROR: %d\r\n", error);
				gn_atem_string_out(buffer);
				return (true);
			}
			break;
		default:
			return (true);
		}
		return (false);
	}

	/* AT+CMGS is sending a message */
	if (strncasecmp(*buf, "MGS", 3) == 0) {
		buf[0] += 3;
		switch (**buf) {
		case '=':
			buf[0]++;
			if (sscanf(*buf, "\"%[+0-9a-zA-Z ]\"", sms.remote.number)) {
				Parser = gn_atem_sms_parseText;
				buf[0] += strlen(*buf);
				gn_atem_string_out("\r\n> ");
			}
			return (true);
		default:
			return (true);
		}
		return (false);
	}

	/* AT+CMGL is listing messages */
	if (strncasecmp(*buf, "MGL", 3) == 0) {
		buf[0] += 3;
		status = -1;

		switch (**buf) {
		case 0:
		case '=':
			buf[0]++;
			/* process <stat> parameter */
			if (*(*buf-1) == 0 || /* i.e. no parameter given */
				strcasecmp(*buf, "1") == 0 ||
				strcasecmp(*buf, "3") == 0 ||
				strcasecmp(*buf, "\"REC READ\"") == 0 ||
				strcasecmp(*buf, "\"STO SENT\"") == 0) {
				status = GN_SMS_Sent;
			} else if (strcasecmp(*buf, "0") == 0 ||
				strcasecmp(*buf, "2") == 0 ||
				strcasecmp(*buf, "\"REC UNREAD\"") == 0 ||
				strcasecmp(*buf, "\"STO UNSENT\"") == 0) {
				status = GN_SMS_Unsent;
			} else if (strcasecmp(*buf, "4") == 0 ||
				strcasecmp(*buf, "\"ALL\"") == 0) {
				/* TODO: in TEXT mode only "ALL" should be
				 * accepted, and in PDU mode only the "4"
				 * form.  */
				status = 4; /* ALL */
			} else {
				return true;
			}
			buf[0] += strlen(*buf);

			/* check all message storages */
			for (index = 1; index <= 20; index++) {

				data.sms->memory_type = SMSType;
				data.sms->number = index;
				error = gn_sms_get(&data, sm);

				switch (error) {
				case GN_ERR_NONE:
					/* print messsage if it has the required status */
					if (data.sms->status == status || status == 4 /* ALL */) {
						gn_atem_sms_print(buffer2, data.sms, MessageFormat);
						gsprintf(buffer, MAX_LINE_LENGTH, "+CMGL: %d,%s\r\n", index, buffer2);
						gn_atem_string_out(buffer);
					}
					break;
				case GN_ERR_EMPTYLOCATION:
					/* don't care if this storage is empty */
					break;
				default:
					/* print other error codes and quit */
					gsprintf(buffer, MAX_LINE_LENGTH, "\r\n+CMS ERROR: %d\r\n", error);
					gn_atem_string_out(buffer);
					return (true);
				}
			}
			break;
		default:
			return (true);
		}
		return (false);
	}

	/* AT+CSCS is character set selection */
	if (strncasecmp(*buf, "SCS", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cscs, data.cscs);
	}

	/* AT+CIMI is international mobile subscriber identity */
	if (strcasecmp(*buf, "IMI") == 0) {
		buf[0] += 3;
		gn_atem_string_out("QEMU_IMSI\r\n");
		return (false);
	}

	/* AT+CMUX is multiplexing mode */
	if (strncasecmp(*buf, "MUX", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cmux, data.cmux);
	}

	/* AT+CMEE is Mobile Termination error reporting */
	if (strncasecmp(*buf, "MEE", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cmee, data.cmee);
	}

	/* AT+CLIP is calling line identification presentation */
	if (strncasecmp(*buf, "LIP", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_clip, data.clip);
	}

	/* AT+COLP is connected line identification presentation */
	if (strncasecmp(*buf, "OLP", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_colp, data.colp);
	}

	/* AT+CSTA is address type selection */
	if (strncasecmp(*buf, "STA", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_csta, data.csta);
	}

	/* AT+CMOD is call mode */
	if (strncasecmp(*buf, "MOD", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cmod, data.cmod);
	}

	/* AT+CBST is bearer service type */
	if (strncasecmp(*buf, "BST", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cbst, data.cbst);
	}

	/* AT+CRLP is radio link protocol */
	if (strncasecmp(*buf, "RLP", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_crlp, data.crlp);
	}

	/* AT+CRC is cellular result codes */
	if (strncasecmp(*buf, "RC", 2) == 0) {
		buf[0] += 2;
		return gn_atem_parse_option(buf, &gn_atem_op_crc, data.crc);
	}

	/* AT+CREG is network registration */
	if (strncasecmp(*buf, "REG", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_creg, data.creg);
	}

	/* AT+CR is reporting control */
	if (strncasecmp(*buf, "R", 1) == 0) {
		buf[0] += 1;
		return gn_atem_parse_option(buf, &gn_atem_op_cr, data.cr);
	}

	/* AT+CEER is extended error report */
	if (strncasecmp(*buf, "EER", 3) == 0) {
		buf[0] += 3;
		gn_atem_string_out("+CEER: 0,0,5,16,normal call clearing\r\n");
		return (false);
	}

	/* AT+CSNS is single numbering scheme */
	if (strncasecmp(*buf, "SNS", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_csns, data.csns);
	}

	/* AT+COPS is PLMN selection */
	if (strncasecmp(*buf, "OPS", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cops, data.cops);
	}

	/* AT+CPAS is phone activity status */
	if (strncasecmp(*buf, "PAS", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cpas, data.cpas);
	}

	/* AT+CFUN is phone functionality */
	if (strncasecmp(*buf, "FUN", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cfun, data.cfun);
	}

	/* AT+CTZR is time zone reporting */
	if (strncasecmp(*buf, "TZR", 3) == 0) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_ctzr, data.ctzr);
	}

	/* AT+CBC is battery charge */
	if (strncasecmp(*buf, "BC", 2) == 0) {
		buf[0] += 2;
		return gn_atem_parse_option(buf, &gn_atem_op_cbc, data.cbc);
	}

	/* AT+CSSN is supplementary service notifications */
	if (strncasecmp(*buf, "CSSN", 4) == 0) {
		buf[0] += 4;
		return gn_atem_parse_option(buf, &gn_atem_op_cssn, data.cssn);
	}

	return (true);
}

static void gn_atem_signal_quality(struct gn_statemachine *state)
{
	float		rflevel = -1;
	gn_rf_unit	rfunits = GN_RF_CSQ;
	char		buffer[MAX_LINE_LENGTH];

	if (!data.csq)
		return;

	data.rf_unit = &rfunits;
	data.rf_level = &rflevel;
	if (gn_sm_functions(GN_OP_GetRFLevel, &data, sm) == GN_ERR_NONE) {
		gsprintf(buffer, MAX_LINE_LENGTH,
				"%%CSQ: %.f, 99, 2\r\n", *(data.rf_level));
		gn_atem_string_out(buffer);
	}
}

/* Handle AT% commands, this is a quick hack together at this
   stage. */
bool	gn_atem_command_percent(char **buf)
{
	char		buffer[MAX_LINE_LENGTH];

	/* This command is undocumented.  */
	if (!strncasecmp(*buf, "CSQ", 3)) {
		buf[0] += 3;
		switch (**buf) {
		case '=':
			buf[0]++;
			switch (**buf) {
			case '0':
				buf[0]++;
				data.csq = 0;
				break;
			case '1':
				buf[0]++;
				data.csq = 1;
				data.signal_quality_notification =
					gn_atem_signal_quality;
				break;
			case '?':
				break;
			default:
				return (true);
			}
			gsprintf(buffer, MAX_LINE_LENGTH,
					"%%CSQ: %i\r\n", data.csq);
			break;
		case '?':
			buf[0]++;
			gsprintf(buffer, MAX_LINE_LENGTH,
					"%%CSQ: (0-31,99),(0-7,99)\r\n");
			gn_atem_string_out(buffer);
			break;
		default:
			return (true);
		}

		return (false);
	}

	/* AT%BAND is Frequency Band Information */
	if (!strncasecmp(*buf, "BAND", 4)) {
		buf[0] += 4;
		return gn_atem_parse_option(buf, &gn_atem_op_band, data.band);
	}

	/* AT%CPI is Call Progress Information */
	if (!strncasecmp(*buf, "CPI", 3)) {
		buf[0] += 3;
		return gn_atem_parse_option(buf, &gn_atem_op_cpi, data.cpi);
	}

	return (true);
}

/* AT+G commands.  Some of these responses are a bit tongue in cheek... */
bool	gn_atem_command_plusg(char **buf)
{
	char		buffer[MAX_LINE_LENGTH];

	/* AT+GMI is Manufacturer information for the TA (Terminal Adaptor) */
	if (strncasecmp(*buf, "MI", 3) == 0) {
		buf[0] += 2;

		gn_atem_string_out(_("Hugh Blemings, Pavel Janik ml. and others...\r\n"));
		return (false);
	}

	/* AT+GMR is Revision information for the TA (Terminal Adaptor) */
	if (strncasecmp(*buf, "MR", 3) == 0) {
		buf[0] += 2;
		gsprintf(buffer, MAX_LINE_LENGTH, "%s %s %s\r\n", VERSION, __TIME__, __DATE__);

		gn_atem_string_out(buffer);
		return (false);
	}

	/* AT+GMM is Model information for the TA (Terminal Adaptor) */
	if (strncasecmp(*buf, "MM", 3) == 0) {
		buf[0] += 2;

		gsprintf(buffer, MAX_LINE_LENGTH, _("gnokii configured on %s for models %s\r\n"), sm->config.port_device, sm->driver.phone.models);
		gn_atem_string_out(buffer);
		return (false);
	}

	/* AT+GSN is Serial number for the TA (Terminal Adaptor) */
	if (strncasecmp(*buf, "SN", 3) == 0) {
		buf[0] += 2;

		gsprintf(buffer, MAX_LINE_LENGTH,
				_("none built in, choose your own\r\n"));
		gn_atem_string_out(buffer);
		return (false);
	}

	/* AT+GCAP is overall capabilities of TA */
	if (strncasecmp(*buf, "CAP", 4) == 0) {
		buf[0] += 3;

		gsprintf(buffer, MAX_LINE_LENGTH, "+GCAP:+CGSM,+FCLASS\r\n");
		gn_atem_string_out(buffer);
		return (false);
	}

	return (true);
}

/* Handle AT# commands */
bool	gn_atem_command_diesis(char **buf)
{
	int	number;
	char	buffer[MAX_LINE_LENGTH];

	if (strncasecmp(*buf, "CID", 3) == 0) {
		buf[0] += 3;
		switch (**buf) {
		case '?':
			buf[0]++;
			gsprintf(buffer, MAX_LINE_LENGTH, "%d\r\n", CallerIDMode);
			gn_atem_string_out(buffer);
			return (false);
		case '=':
			buf[0]++;
			if (**buf == '?') {
				buf[0]++;
				gn_atem_string_out("0,1\r\n");
				return (false);
			} else {
				number = gn_atem_num_get(buf);
				if ( number == 0 || number == 1 ) {
					CallerIDMode = number;
					return (false);
				}
			}
		}
	}
	return (true);
}

/* Send a result string back.  There is much work to do here, see
   the code in the isdn driver for an idea of where it's heading... */
void	gn_atem_modem_result(int code)
{
	char	buffer[16];

	gn_atem_string_out("\r\n");	/* XXX Some modems do this */

	if (!(ModemRegisters[REG_VERBOSE] & BIT_VERBOSE)) {
		sprintf(buffer, "%d\r\n", code);
		gn_atem_string_out(buffer);
	} else {
		switch (code) {
			case MR_OK:
				gn_atem_string_out("OK\r\n");
				break;

			case MR_ERROR:
				gn_atem_string_out("ERROR\r\n");
				break;

			case MR_CARRIER:
				gn_atem_string_out("CARRIER\r\n");
				break;

			case MR_CONNECT:
				gn_atem_string_out("CONNECT\r\n");
				break;

			case MR_NOCARRIER:
				gn_atem_string_out("NO CARRIER\r\n");
				break;

		        case MR_RING:
				gn_atem_cpi(GSMD_CALLPROG_SETUP,
						GSMD_CALL_DIR_MT, 0);
				gn_atem_cpi(GSMD_CALLPROG_SETUP,
						GSMD_CALL_DIR_MT, 1);
				if (!strcmp(data.crc, "1"))
					gn_atem_string_out(
							"+CRING: VOICE\r\n");
				else
					gn_atem_string_out("RING\r\n");
				/* XXX: Standard format is "313373",145 */
				if (!strcmp(data.clip, "1,1"))
					gn_atem_string_out("+CLIP: \"313373\""
							",145,,,,1\r\n");
				gn_atem_cpi(GSMD_CALLPROG_SYNC,
						GSMD_CALL_DIR_MT, 1);
				break;

			default:
				gn_atem_string_out(_("\r\nUnknown Result Code!\r\n"));
				break;
		}
	}

}


/* Get integer from char-pointer, set pointer to end of number
   stolen basically verbatim from ISDN code.  */
int gn_atem_num_get(char **p)
{
	int v = -1;

	while (*p[0] >= '0' && *p[0] <= '9') {
		v = ((v < 0) ? 0 : (v * 10)) + (int) ((*p[0]++) - '0');
	}

	return v;
}

/* Write string to virtual modem port, either pty or
   STDOUT as appropriate.  This function is only used during
   command mode - data pump is used when connected.  */
void	gn_atem_string_out(char *buffer)
{
	char	out_char;

	while (*buffer) {
		/* Translate CR/LF/BS as appropriate */
		switch (*buffer) {
		case '\r':
			out_char = ModemRegisters[REG_CR];
			break;
		case '\n':
			out_char = ModemRegisters[REG_LF];
			break;
		case '\b':
			out_char = ModemRegisters[REG_BS];
			break;
		default:
			out_char = *buffer;
			break;
		}

		sm->info->write(sm->info->opaque, "%c", out_char);
		buffer ++;
	}
}
