/* $Id$ G N O K I I A Linux/Unix toolset and driver for Nokia mobile phones. Released under the terms of the GNU GPL, see file COPYING for more details. 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 "config.h" #define __data_at_emulator_c #include #include #include #include #include #include #include #include #include #include #include #ifndef WIN32 #include #endif #include "config.h" #include "misc.h" #include "gsm-common.h" #include "gsm-api.h" #include "data/at-emulator.h" #include "data/virtmodem.h" #include "data/datapump.h" #define MAX_LINE_LENGTH 256 /* Global variables */ bool ATEM_Initialised = false; /* Set to true once initialised */ extern bool CommandMode; extern int ConnectCount; static char ModelName[80]; /* This seems to be needed to avoid seg-faults */ static char PortName[80]; /* Local variables */ extern int PtyRDFD; /* File descriptor for reading and writing to/from */ extern int PtyWRFD; /* pty interface - only different in debug mode. */ static u8 ModemRegisters[MAX_MODEM_REGISTERS]; static char CmdBuffer[MAX_CMD_BUFFERS][CMD_BUFFER_LENGTH]; static int CurrentCmdBuffer; static int CurrentCmdBufferIndex; static bool VerboseResponse; /* Switch betweek numeric (4) and text responses (ERROR) */ static char IncomingCallNo; static int MessageFormat; /* Message Format (text or pdu) */ static void ATEM_ParseAT(char *cmd_buffer); /* Current command parser */ static void (*Parser)(char *); //void (*Parser)(char *) = ATEM_ParseAT; /* Current command parser */ static GSM_MemoryType SMSType; static int SMSNumber; static void ATEM_InitRegisters(void); /* If initialised in debug mode, stdin/out is used instead of ptys for interface. */ bool ATEM_Initialise(int read_fd, int write_fd, char *model, char *port) { PtyRDFD = read_fd; PtyWRFD = write_fd; strncpy(ModelName,model,80); strncpy(PortName,port,80); /* Initialise command buffer variables */ CurrentCmdBuffer = 0; CurrentCmdBufferIndex = 0; /* Default to verbose reponses */ VerboseResponse = true; /* Initialise registers */ ATEM_InitRegisters(); /* Initial parser is AT routine */ Parser = ATEM_ParseAT; /* Setup defaults for AT*C interpreter. */ SMSNumber = 1; SMSType = GMT_ME; /* Default message format is PDU */ MessageFormat = PDU_MODE; /* Set the call passup so that we get notified of incoming calls */ GSM->DialData(NULL,-1,&ATEM_CallPassup); /* We're ready to roll... */ ATEM_Initialised = true; return (true); } /* Initialise the "registers" used by the virtual modem. */ static void ATEM_InitRegisters(void) { ModemRegisters[REG_RINGATA] = 0; ModemRegisters[REG_RINGCNT] = 2; ModemRegisters[REG_ESC] = '+'; ModemRegisters[REG_CR] = 10; ModemRegisters[REG_LF] = 13; ModemRegisters[REG_BS] = 8; ModemRegisters[S35]=7; ModemRegisters[REG_ECHO] = BIT_ECHO; } /* This gets called to indicate an incoming call */ void ATEM_CallPassup(char c) { if ((c >= 0) && (c < 9)) { ATEM_ModemResult(MR_RING); IncomingCallNo = c; } } static void ATEM_StringOut(char *buffer); /* Handler called when characters received from serial port. calls state machine code to process it. */ void ATEM_HandleIncomingData(char *buffer, int length) { int count; unsigned char out_buf[3]; for (count = 0; count < length ; count++) { /* Echo character if appropriate. */ if (ModemRegisters[REG_ECHO] & BIT_ECHO) { out_buf[0] = buffer[count]; out_buf[1] = 0; ATEM_StringOut(out_buf); } /* 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]) { CmdBuffer[CurrentCmdBuffer][CurrentCmdBufferIndex] = 0x00; Parser(CmdBuffer[CurrentCmdBuffer]); CurrentCmdBuffer++; if (CurrentCmdBuffer >= MAX_CMD_BUFFERS) { CurrentCmdBuffer = 0; } CurrentCmdBufferIndex = 0; } else { CmdBuffer[CurrentCmdBuffer][CurrentCmdBufferIndex] = buffer[count]; CurrentCmdBufferIndex++; if (CurrentCmdBufferIndex >= CMD_BUFFER_LENGTH) { CurrentCmdBufferIndex = CMD_BUFFER_LENGTH; } } } } static bool ATEM_CommandPlusC(char **buf); static bool ATEM_CommandPlusG(char **buf); static int ATEM_GetNum(char **p); static void ATEM_ParseSMS(char *cmd_buffer); /* Parser for standard AT commands. cmd_buffer must be null terminated. */ static void ATEM_ParseAT(char *cmd_buffer) { char *buf; char number[30]; if (strncasecmp (cmd_buffer, "AT", 2) != 0) { ATEM_ModemResult(MR_ERROR); return; } for (buf = &cmd_buffer[2]; *buf;) { switch (toupper(*buf)) { case 'Z': buf++; break; case 'A': buf++; /* For now we'll also initialise the datapump + rlp code again */ DP_Initialise(PtyRDFD, PtyWRFD); GSM->DialData(NULL, -1, &DP_CallPassup); GSM->AnswerCall(IncomingCallNo); CommandMode = false; return; break; case 'D': /* Dial Data :-) */ /* FIXME - should parse this better */ /* For now we'll also initialise the datapump + rlp code again */ DP_Initialise(PtyRDFD, PtyWRFD); buf++; if (toupper(*buf) == 'T') buf++; if (*buf == ' ') buf++; strncpy(number, buf, 30); if (ModemRegisters[S35] == 0) GSM->DialData(number, 1, &DP_CallPassup); else GSM->DialData(number, 0, &DP_CallPassup); ATEM_StringOut("\n\r"); CommandMode = false; return; break; case 'H': /* Hang Up */ buf++; RLP_SetUserRequest(Disc_Req, true); GSM->CancelCall(); break; case 'S': /* Change registers - only no. 35 for now */ buf++; if (memcmp(buf, "35=", 3) == 0) { buf += 3; ModemRegisters[S35] = *buf - '0'; buf++; } break; /* E - Turn Echo on/off */ case 'E': buf++; switch (ATEM_GetNum(&buf)) { case 0: ModemRegisters[REG_ECHO] &= ~BIT_ECHO; break; case 1: ModemRegisters[REG_ECHO] |= BIT_ECHO; break; default: ATEM_ModemResult(MR_ERROR); return; } break; /* Handle AT* commands (Nokia proprietary I think) */ case '*': buf++; if (!strcasecmp(buf, "NOKIATEST")) { ATEM_ModemResult(MR_OK); /* FIXME? */ return; } else { if (!strcasecmp(buf, "C")) { ATEM_ModemResult(MR_OK); Parser = ATEM_ParseSMS; return; } } break; /* + is the precursor to another set of commands +CSQ, +FCLASS etc. */ case '+': buf++; switch (toupper(*buf)) { case 'C': buf++; /* Returns true if error occured */ if (ATEM_CommandPlusC(&buf) == true) { return; } break; case 'G': buf++; /* Returns true if error occured */ if (ATEM_CommandPlusG(&buf) == true) { return; } break; default: ATEM_ModemResult(MR_ERROR); return; } break; default: ATEM_ModemResult(MR_ERROR); return; } } ATEM_ModemResult(MR_OK); } static GSM_Error ATEM_ReadSMS(int number, GSM_MemoryType type, GSM_SMSMessage *message) { GSM_Error error; message->MemoryType = type; message->Location = number; error = GSM->GetSMSMessage(message); return error; } static void ATEM_PrintSMS(char *line, GSM_SMSMessage *message, int mode) { switch (mode) { case INTERACT_MODE: gsprintf(line, MAX_LINE_LENGTH, _("\n\rDate/time: %d/%d/%d %d:%02d:%02d Sender: %s Msg Center: %s\n\rText: %s\n\r"), message->Time.Day, message->Time.Month, message->Time.Year, message->Time.Hour, message->Time.Minute, message->Time.Second, message->Sender, message->MessageCenter.Number, message->MessageText); break; case TEXT_MODE: if (message->Coding==GSM_Coding_8bit) gsprintf(line, MAX_LINE_LENGTH, _("\"%s\",\"%s\",,\"%02d/%02d/%02d,%02d:%02d:%02d+%02d\"\n\r%s"), (message->Status ? _("REC READ") : _("REC UNREAD")), message->Sender, message->Time.Year, message->Time.Month, message->Time.Day, message->Time.Hour, message->Time.Minute, message->Time.Second, message->Time.Timezone, _("")); else gsprintf(line, MAX_LINE_LENGTH, _("\"%s\",\"%s\",,\"%02d/%02d/%02d,%02d:%02d:%02d+%02d\"\n\r%s"), (message->Status ? _("REC READ") : _("REC UNREAD")), message->Sender, message->Time.Year, message->Time.Month, message->Time.Day, message->Time.Hour, message->Time.Minute, message->Time.Second, message->Time.Timezone, message->MessageText); break; case PDU_MODE: gsprintf(line, MAX_LINE_LENGTH, _("")); break; default: gsprintf(line, MAX_LINE_LENGTH, _("")); break; } } static void ATEM_EraseSMS(int number, GSM_MemoryType type) { GSM_SMSMessage message; message.MemoryType = type; message.Location = number; if (GSM->DeleteSMSMessage(&message) == GE_NONE) { ATEM_ModemResult(MR_OK); } else { ATEM_ModemResult(MR_ERROR); } } static void ATEM_HandleSMS() { GSM_SMSMessage message; GSM_Error error; char buffer[MAX_LINE_LENGTH]; error = ATEM_ReadSMS(SMSNumber, SMSType, &message); switch (error) { case GE_NONE: ATEM_PrintSMS(buffer, &message, INTERACT_MODE); ATEM_StringOut(buffer); break; default: gsprintf(buffer, MAX_LINE_LENGTH, _("\n\rNo message under number %d\n\r"), SMSNumber); ATEM_StringOut(buffer); break; } return; } static void ATEM_ParseDIR(char *cmd_buffer); /* Parser for SMS interactive mode */ static void ATEM_ParseSMS(char *buff) { if (!strcasecmp(buff, "HELP")) { ATEM_StringOut(_("\n\rThe following commands work...\n\r")); ATEM_StringOut("DIR\n\r"); ATEM_StringOut("EXIT\n\r"); ATEM_StringOut("HELP\n\r"); return; } if (!strcasecmp(buff, "DIR")) { SMSNumber = 1; ATEM_HandleSMS(); Parser = ATEM_ParseDIR; return; } if (!strcasecmp(buff, "EXIT")) { Parser = ATEM_ParseAT; ATEM_ModemResult(MR_OK); return; } ATEM_ModemResult(MR_ERROR); } /* Parser for DIR sub mode of SMS interactive mode. */ static void ATEM_ParseDIR(char *buff) { switch (toupper(*buff)) { case 'P': SMSNumber--; ATEM_HandleSMS(); return; case 'N': SMSNumber++; ATEM_HandleSMS(); return; case 'D': ATEM_EraseSMS(SMSNumber, SMSType); return; case 'Q': Parser= ATEM_ParseSMS; ATEM_ModemResult(MR_OK); return; } ATEM_ModemResult(MR_ERROR); } /* Handle AT+C commands, this is a quick hack together at this stage. */ static bool ATEM_CommandPlusC(char **buf) { float rflevel; GSM_RFUnits rfunits = GRF_CSQ; char buffer[MAX_LINE_LENGTH], buffer2[MAX_LINE_LENGTH]; int status, index; GSM_Error error; GSM_SMSMessage message; if (strncasecmp(*buf, "SQ", 2) == 0) { buf[0] += 2; if (GSM->GetRFLevel(&rfunits, &rflevel) == GE_NONE) { gsprintf(buffer, MAX_LINE_LENGTH, "\n\r+CSQ: %0.0f, 99", rflevel); ATEM_StringOut(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; ATEM_StringOut(_("\n\rNokia Mobile Phones")); return (false); } /* AT+CGSN is IMEI */ if (strncasecmp(*buf, "GSN", 3) == 0) { buf[0] += 3; if (GSM->GetIMEI(buffer2) == GE_NONE) { gsprintf(buffer, MAX_LINE_LENGTH, "\n\r%s", buffer2); ATEM_StringOut(buffer); return (false); } else { return (true); } } /* AT+CGMR is Revision (hardware) */ if (strncasecmp(*buf, "GMR", 3) == 0) { buf[0] += 3; if (GSM->GetRevision(buffer2) == GE_NONE) { gsprintf(buffer, MAX_LINE_LENGTH, "\n\r%s", buffer2); ATEM_StringOut(buffer); return (false); } else { return (true); } } /* AT+CGMM is Model code */ if (strncasecmp(*buf, "GMM", 3) == 0) { buf[0] += 3; if (GSM->GetModel(buffer2) == GE_NONE) { gsprintf(buffer, MAX_LINE_LENGTH, "\n\r%s", buffer2); ATEM_StringOut(buffer); return (false); } else { return (true); } } /* 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, "\n\r+CMGF: %d", MessageFormat); ATEM_StringOut(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); error = ATEM_ReadSMS(index, SMSType, &message); switch (error) { case GE_NONE: ATEM_PrintSMS(buffer2, &message, MessageFormat); gsprintf(buffer, MAX_LINE_LENGTH, "\n\r+CMGR: %s", buffer2); ATEM_StringOut(buffer); break; default: gsprintf(buffer, MAX_LINE_LENGTH, "\n\r+CMS ERROR: %d\n\r", error); ATEM_StringOut(buffer); return (true); } break; 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 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 = GSS_SENTREAD; } else if (strcasecmp(*buf, "0") == 0 || strcasecmp(*buf, "2") == 0 || strcasecmp(*buf, "\"REC UNREAD\"") == 0 || strcasecmp(*buf, "\"STO UNSENT\"") == 0) { status = GSS_NOTSENTREAD; } else if (strcasecmp(*buf, "4") == 0 || strcasecmp(*buf, "\"ALL\"") == 0) { status = 4; /* ALL */ } else { return true; } buf[0] += strlen(*buf); /* check all message storages */ for (index = 1; index <= 20; index++) { error = ATEM_ReadSMS(index, SMSType, &message); switch (error) { case GE_NONE: /* print messsage if it has the required status */ if (message.Status == status || status == 4 /* ALL */) { ATEM_PrintSMS(buffer2, &message, MessageFormat); gsprintf(buffer, MAX_LINE_LENGTH, "\n\r+CMGL: %d,%s", index, buffer2); ATEM_StringOut(buffer); } break; case GE_EMPTYSMSLOCATION: /* don't care if this storage is empty */ break; default: /* print other error codes and quit */ gsprintf(buffer, MAX_LINE_LENGTH, "\n\r+CMS ERROR: %d\n\r", error); ATEM_StringOut(buffer); return (true); } } break; default: return (true); } return (false); } return (true); } /* AT+G commands. Some of these responses are a bit tongue in cheek... */ static bool ATEM_CommandPlusG(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; ATEM_StringOut(_("\n\rHugh Blemings, Pavel Janík ml. and others...")); 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, "\n\r%s %s %s", VERSION, __TIME__, __DATE__); ATEM_StringOut(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, _("\n\rgnokii configured for %s on %s"), ModelName, PortName); ATEM_StringOut(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, _("\n\rnone built in, choose your own")); ATEM_StringOut(buffer); 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 ATEM_ModemResult(int code) { char buffer[16]; if (VerboseResponse == false) { sprintf(buffer, "\n\r%d\n\r", code); ATEM_StringOut(buffer); } else { switch (code) { case MR_OK: ATEM_StringOut("\n\rOK\n\r"); break; case MR_ERROR: ATEM_StringOut("\n\rERROR\n\r"); break; case MR_CARRIER: ATEM_StringOut("\n\rCARRIER\n\r"); break; case MR_CONNECT: ATEM_StringOut("\n\rCONNECT\n\r"); break; case MR_NOCARRIER: ATEM_StringOut("\n\rNO CARRIER\n\r"); break; case MR_RING: ATEM_StringOut("RING\n\r"); break; default: ATEM_StringOut(_("\n\rUnknown Result Code!\n\r")); break; } } } /* Get integer from char-pointer, set pointer to end of number stolen basically verbatim from ISDN code. */ static int ATEM_GetNum(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. */ static void ATEM_StringOut(char *buffer) { int count = 0; char out_char; while (count < strlen(buffer)) { /* Translate CR/LF/BS as appropriate */ switch (buffer[count]) { 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[count]; break; } write(PtyWRFD, &out_char, 1); count ++; } }