Схема работы достаточно проста. Общение с модемом происходит посредством записи в него АТ-команд и чтения ответов.
Для посылки SMS используется следующая последовательность команд (её даже можно назвать сценарием):
Посылаем начальный символ <ESC> и переключаем модем в режим подробного вывода:
<ESC>AT+CMEE=2
Отключаем эхо:
ATE0
Инициализируем модем:
AT
Переключаем в текстовый режим передачи:
AT+CMGF=1
Передаём номер телефона <number> в модем:
AT+CMGS="<number>"
Передаём текст сообщения <message> в модем:
<message>
Посылаем сообщение:
<^Z>
Здесь
<ESC> и <^Z> - это символы с кодами 0x1B и 0x1A, соответственно;
<number> - номер телефона;
<message> - текст сообщения.
Переносы строк важно соблюдать, в коде им соответствует символ '\r'. Текст сообщения также закачивается символом '\r'.
Сам модем отвечает на некоторые команды, это отражено в структуре zbx_sms_scenario:
typedef struct
{
const char *message;
const char *result;
int timeout_sec;
}
zbx_sms_scenario;
Здесь message — это команда, result — ожидаемый ответ модема, а timeout_sec — и так понятно — тайм-аут в секундах.
Заполненные структуры размещается в массиве scenario:
zbx_sms_scenario scenario[] =
{
{"\x1B" , NULL , 0} , /* Send <ESC> */
{"AT+CMEE=2\r" , "" /*"OK"*/, 5} , /* verbose error values */
{"ATE0\r" , "OK" , 5} , /* Turn off echo */
{"AT\r" , "OK" , 5} , /* Init modem */
{"AT+CMGF=1\r" , "OK" , 5} , /* Switch to text mode */
{"AT+CMGS=\"" , NULL , 0} , /* Set phone number */
{number , NULL , 0} , /* Write phone number */
{"\"\r" , "> " , 5} , /* Set phone number */
{message , NULL , 0} , /* Write message */
{"\x1A" , "+CMGS: " , 40} , /* Send message ^Z */
{NULL , "OK" , 1} ,
{NULL , NULL , 0}
};
Для общения с модемом используются две функции (некоторые детали опущены для упрощения восприятия):
- Функция
write_gsm(int fd, const char *str) осуществляет запись строки str в открытый дескриптор fd модема.
- Функция
read_gsm(int fd, const char *expect, int timeout_sec) читает ответ из дескриптора fd модема и сравнивает его с ожидаемым ответом expect; возвращает ошибку, если истекает тайм-аут timeout_sec.
write_gsm()
Запись в модем выполняется циклически, пока не будет записан весь буфер str:
int i, wlen, len;
len = strlen(str);
for (wlen = 0; wlen < len; wlen += i)
{
if (-1 == (i = write(fd, str + wlen, len - wlen)))
{
i = 0;
if (EAGAIN == errno)
continue;
return FAIL;
}
}
return SUCCEED;
read_gsm()
Тайм-аут реализован через системный вызов select(), необходимые для вызова структуры инициализируются просто:
fd_set fdset;
struct timeval tv;
tv.tv_sec = timeout_sec;
tv.tv_usec = 0;
FD_ZERO(&fdset);
FD_SET(fd, &fdset);
Сам select сидит в бесконечном цикле на случай прерываний сигналами. В случае ошибки или превышения тайм-аута, возвращается FAIL, соответственно, код вне цикла выполнится только в случае успеха.
int rc;
while (1)
{
rc = select(fd + 1, &fdset, NULL, NULL, &tv);
if (-1 == rc)
{
if (EINTR == errno)
continue;
return FAIL;
}
else if (0 == rc) /* timeout exceeded */
{
return FAIL;
}
else
break;
}
После возврата из select() начинаем читать и продолжаем до тех пор, пока есть что:
static char buffer[0xff], *ebuf = buffer;
nbytes_total = 0;
while (0 < (nbytes = read(fd, ebuf, buffer + sizeof(buffer) - 1 - ebuf)))
{
ebuf += nbytes;
*ebuf = '\0';
nbytes_total += nbytes;
}
Проверка значения осуществляется вполне просто: сверяем ответ модема с ожидаемым:
return (NULL == strstr(*ebuf, expect)) ? FAIL : SUCCEED;
Теперь, когда разобрались как работают чтение/запись, можно посмотреть как это использовать.
Массив scenario обходим циклом. Поля структуры zbx_sms_scenario интерпретируются следующим образом: если поле message не NULL — требуется послать его содержимое в модем через write_gsm(), если поле result не NULL — нужно дождаться соответствующего ответа модема через read_gsm(). Код этого дела:
zbx_sms_scenario *step;
for (step = scenario; NULL != step->message || NULL != step->result; step++)
{
if (NULL != step->message)
{
if (message == step->message)
{
char *tmp;
tmp = zbx_strdup(NULL, message);
zbx_remove_chars(tmp, "\r");
ret = write_gsm(f, tmp);
zbx_free(tmp);
}
else
ret = write_gsm(f, step->message);
if (FAIL == ret)
break;
}
if (NULL != step->result)
{
if (FAIL == (ret = read_gsm(f, step->result, step->timeout_sec)))
break;
}
}
В этой части кода используются функции zbx_strdup(), zbx_remove_chars() и zbx_free() для того чтобы вырезать из исходного сообщения символ '\r', который как уже было сказано является признаком конца сообщения.
Если в процессе «общения» возникает ошибка (return_code == FAIL), в модем посылается последовательность
<CR><ESC><^Z>,
прерывающая дальнейшее выполнение команд; ответ модема в данном случае мало информативен и последующий вызов read_gsm() просто от него избавляется:
if (FAIL == ret)
{
write_gsm(f, "\r\xB2\xB1"); /* cancel all */
read_gsm(f, "", 0); /* clear buffer */
}
Следует, пожалуй, отметить, что посылать таким образом SMS можно, только с английскими символами. Как слать русские буквы — уже другая песня.
Ну вот и всё, рассмотренного кода достаточно, чтобы уже завтра привинтить SMS-оповещения к своему величественному проекту на С и радоваться жизни в новых красках.