Схема работы достаточно проста. Общение с модемом происходит посредством записи в него АТ-команд и чтения ответов.
Для посылки 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-оповещения к своему величественному проекту на С и радоваться жизни в новых красках.