null

SMS-оповещения в Zabbix 3.0

Схема работы достаточно проста. Общение с модемом происходит посредством записи в него АТ-команд и чтения ответов.

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

Назад