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