Как работает Quectel OpenCPU RIL

В данной статье более подробно хочу рассмотреть, как работает такая важная вещь Quectel OpenCPU, как RIL, разберем всю физику и попробуем расширить возможности добавлением собственных API функций. Я разбираю данную тему для конкретного модуля, а именно Quectel M66, но для остальных модулей ситуация аналогична.

Чтобы понемногу войти в курс дела того, как работает RIL, стоит обратиться к основной терминологии:

RIL (Radio Interface Layer) — это пользовательский API для управления модулем. Разработчик может вызывать API вызовы и отправлять, ассоциированные с ними AT команды, после чего получать на них ответ.

URC (Unsolicited Result Code) — сообщения, которые получает приложение пользователя через механизм сообщений MSG_ID_URC_INDICATION.

OpenCPU RIL, главным образом, предоставляет 2 API функции и 2 системных сообщения для приложения. Все файлы RIL находятся в директории /ril SDK.

RIL сервисОписание
Сообщение: MSG_ID_RIL_READYМожет быть получено в основном потоке приложения, когда RIL готов к работе
Сообщение: MSG_ID_URC_INDICATIONСообщения, которые получает основной поток приложения, когда генерируются URC коды. В данном сообщении parameter1 это тип URC и parameter2 несет в себе специфичную информацию для данного типа URC
API: Ql_RIL_InitializeВ главном потоке программы необходимо вызвать данную функцию для инициализации RIL API при получении сообщения MSG_ID_RIL_READY. AT команды инициализации, которые объявлены в g_InitCmds будут вызваны
API: Ql_RIL_SendATCmdФункция отправки AT команд, с синхронно-возвращаемым результатом

C функцией Ql_RIL_Initialize все более менее понятно, она не принимает никаких параметров и ничего не возвращает, в теле функции происходит вызов некоторых AT команд инициализации из g_InitCmds. Собственно, вот этот список команд, определенный в файле ril_init.c:

const char* g_InitCmds[] = {
     //"ATE0Q0V1\r",   // verbose result codes
    "AT+CMEE=1\r",     // Extended errors. This item is necessary.     
    "ATS0=0\r",        // No auto-answer.  If customer want auto answer the incoming call , must change this string as "ATS0=n\r" (n=1~255).
    "AT+CREG=1\r",     // GSM registration events . 
    "AT+CGREG=1\r",    // GPRS registration events
    "AT+CLIP=1\r",     // Display RING number
    "AT+COLP=0\r"      // no connected line identification

//......  More customization setting can add here
};

Что интересно, в данный массив можно добавить свои собственные AT команды, которые вы хотите чтобы были выполнены в процессе инициализации, а вот убирать что-то из того, что уже есть, крайне не рекомендуется.

А вот с функцией Ql_RIL_SendATCmd все не так очевидно, как кажется на первый взгляд. Функция отправляет AT команду и синхронно возвращает результат вызова этой команды. Перед тем как функция что-либо возвращает, ответы на AT команду обрабатываются в функции обратного вызова atRsp_callback и необходимый результат запроса может быть сохранен в памяти, на которую указывает параметр userData. Все ответы на AT команду передаются в функцию обратного вызова строка за строкой, таким образом данная функция может быть вызвана несколько раз, в зависимости от длины ответа на конкретную AT команду.

s32 Ql_RIL_SendATCmd(char* atCmd,
                     u32 atCmdLen,
                     Callback_ATResponse atRsp_callback,
                     void* userData,
                     u32 timeout
                     );
typedef s32 (*Callback_ATResponse)(char* line, u32 len, void* userdata);

Параметры:
atCmd указатель на строку с AT командой;
atCmdLen длина строки с AT командой;
atRsp_callback функция обратной связи для обработки ответа;
userData указатель на пользовательский параметр, в который сохраняется результат запроса;
timeOut таймаут ожидания ответа в милисекундах, если установлен в 0, то берется значение по умолчанию для RIL, равное 3 минутам.

Возвращаемое значение:
RIL_AT_SUCCESS возвращается при успешной отправке AT команды и получении ответа OK;
RIL_AT_FAILED не удалось отправить команду или, что более часто бывает, получен ответ ERROR;
RIL_AT_TIMEOUT завершен таймаут передачи;
RIL_AT_BUSY возвращается, если в данный момент идет отправка другой AT команды;
RIL_AT_INVALID_PARAM неверный входной параметр;
RIL_AT_UNINITIALIZED не готова работа RIL, надо дождаться сообщения MSG_ID_RIL_READY, потом вызвать Ql_RIL_Initialize

В некоторых случаях вместо указателя на функцию обратного вызова передается NULL, в этом случае вызывается функция обратного вызова по умолчанию из файла ril_atResponse.c, данная функция может лишь обрабатывать простые AT команды, которые возвращают лишь OK и ERROR:

s32 Default_atRsp_callback(char* line, u32 len, void* userdata)
{
    if (Ql_RIL_FindLine(line, len, "OK"))    // find <CR><LF>OK<CR><LF>,OK<CR><LF>, <CR>OK<CR>£¬<LF>OK<LF>
    {
        m_iErrCode = RIL_ATRSP_SUCCESS;
        return  RIL_ATRSP_SUCCESS;
    }
    else if (Ql_RIL_FindLine(line, len, "ERROR"))     // find <CR><LF>ERROR<CR><LF>, <CR>ERROR<CR>£¬<LF>ERROR<LF>
    {
        m_iErrCode = RIL_ATRSP_FAILED;
        return  RIL_ATRSP_FAILED;
    }
    else if (Ql_RIL_FindString(line, len, "+CME ERROR:") || 
             Ql_RIL_FindString(line, len, "+CMS ERROR:"))
    {
        Ql_sscanf(line, "%*[^:]: %d\r\n", &m_iErrCode);
        return  RIL_ATRSP_FAILED;
    }
    return RIL_ATRSP_CONTINUE;    //continue wait
}

Используя RIL API, надо иметь в виду что, если одна функция не вернула ответ, то другая задача, которая вызывает функцию RIL, будет ожидать пока не будет возвращено значение предыдущего запроса. Ну это так, чтобы вы не удивлялись.

Выше я уже говорил, что перед использованием RIL API необходимо проинициализировать RIL после получения сообщения MSG_ID_RIL_READY. Делается это так:

void proc_main_task(s32 taskId)
{
    s32 ret;
    ST_MSG msg;

    while (TRUE)
    {
        Ql_OS_GetMessage(&msg);
        switch (msg.message)
        {
#ifdef __OCPU_RIL_SUPPORT__
            case MSG_ID_RIL_READY:
                Ql_Debug_Trace("<-- RIL is ready -->\r\n");
                ret = Ql_RIL_Initialize();
                break;
#endif
            default:
                break;
        }
    }
}

Немного об URC

В самом начале я прописал в терминах URC, это те сообщения, которые получает ваше приложение через механизм MSG_ID_URC_INDICATION. Каждое такое сообщение, которое может быть получено, состоит из двух параметров, которые так и называются parameter1 и parameter2. Вот эти параметры и необходимо правильно интерпретировать в своей программе.

Имеется интересная особенность URC сообщений, которая не всегда на виду, а именно, есть два типа таких сообщений: системные, в которых возвращается статус работы модуля и AT URC, которые обрабатывают некоторые специфичные AT команды. Обычно, это те команды, время ответа на которые, больше времени ожидания ответа по умолчанию, например, "AT+QNTP="

Вот фрагмент кода, который занимается как раз интерпретацией полученных URC сообщений. Здесь не все возможные сообщения, разработчик может добавить еще и другие по своему усмотрению:

void proc_main_task(s32 taskId)
{
    s32 ret;
    ST_MSG msg;

    // Register & open UART port
    ret = Ql_UART_Register(m_myUartPort, CallBack_UART_Hdlr, NULL);
    if (ret < QL_RET_OK)
    {
        Ql_Debug_Trace("Fail to register serial port[%d], ret=%d\r\n", m_myUartPort, ret);
    }
    ret = Ql_UART_Open(m_myUartPort, 115200, FC_NONE);
    if (ret < QL_RET_OK)
    {
        Ql_Debug_Trace("Fail to open serial port[%d], ret=%d\r\n", m_myUartPort, ret);
    }

    Ql_Debug_Trace("OpenCPU: Customer Application\r\n");

    // START MESSAGE LOOP OF THIS TASK
    while(TRUE)
    {
        Ql_OS_GetMessage(&msg);
        switch(msg.message)
        {
        case MSG_ID_RIL_READY:
            Ql_Debug_Trace("<-- RIL is ready -->\r\n");
            Ql_RIL_Initialize();
            break;
        case MSG_ID_URC_INDICATION:
            //Ql_Debug_Trace("<-- Received URC: type: %d, -->\r\n", msg.param1);
            switch (msg.param1)
            {
            case URC_SYS_INIT_STATE_IND:
                Ql_Debug_Trace("<-- Sys Init Status %d -->\r\n", msg.param2);
                break;
            case URC_SIM_CARD_STATE_IND:
                Ql_Debug_Trace("<-- SIM Card Status:%d -->\r\n", msg.param2);
                break;
            case URC_GSM_NW_STATE_IND:
                Ql_Debug_Trace("<-- GSM Network Status:%d -->\r\n", msg.param2);
                break;
            case URC_GPRS_NW_STATE_IND:
                Ql_Debug_Trace("<-- GPRS Network Status:%d -->\r\n", msg.param2);
                break;
            case URC_CFUN_STATE_IND:
                Ql_Debug_Trace("<-- CFUN Status:%d -->\r\n", msg.param2);
                break;
            case URC_COMING_CALL_IND:
                {
                    ST_ComingCall* pComingCall = (ST_ComingCall*)msg.param2;
                    Ql_Debug_Trace("<-- Coming call, number:%s, type:%d -->\r\n", pComingCall->phoneNumber, pComingCall->type);
                    break;
                }
            case URC_CALL_STATE_IND:
                Ql_Debug_Trace("<-- Call state:%d\r\n", msg.param2);
                break;
            case URC_NEW_SMS_IND:
                Ql_Debug_Trace("<-- New SMS Arrives: index=%d\r\n", msg.param2);
                break;
            case URC_MODULE_VOLTAGE_IND:
                Ql_Debug_Trace("<-- VBatt Voltage Ind: type=%d\r\n", msg.param2);
                break;
            default:
                Ql_Debug_Trace("<-- Other URC: type=%d\r\n", msg.param1);
                break;
            }
            break;
        default:
            break;
        }
    }
}

Параметр msg.param2 отличается от URC к URC, к примеру, для URC URC_GPRS_NW_STATE_IND этот параметр дает информацию о состоянии GPRS сети, а это не что иное, как 32-битный integer из Enum_NetworkState. А URC_COMING_CALL_IND имеет msg.param2 из ST_ComingCall.

Все системные URC сообщения определены в массиве констант m_SysURCHdlEntry файла ril_urc.c. Все AT URC определены в m_AtURCHdlEntry массиве и, если разработчик желает, он может сюда добавить реализацию метода для существующего AT URC сообщения, предварительно реализовав этот метод.

Разрабатываем свой RIL API

Несмотря на то, что Quectel заявляет, что все функции для работы с телефонией и SMS реализованы в RIL, некоторых все же нет и сегодня попробуем реализовать одну из таких.

Задача определять, момент, когда абонент снимает трубку при исходящем вызове с модема, очень специфичная, но иногда встречается. Т.е. нам нужно научиться определять состояния исходящего вызова, такие как «осуществляется дозвон», «получен ответ станции», «абонент поднял трубку» и «вызов в режиме ожидания». Для этого есть специальная AT команда AT+CLCC, которая возвращает список текущих вызовов в виде [+CLCC: <id1>,<dir>,<stat>,<mode>,<mpty>[,<number>,<type>[,""]], где:

ПараметрОписаниеВозможные значения
<id1>Идентификатор вызова
<dir>Направление вызова0 Исходящий вызов
1 Входящий вызов
<stat>Состояние вызова0 Active — Трубка поднята
1 Held — В состоянии удержания вызова
2 Dialing (MO call) — Набор номера осуществлен
3 Alerting (MO call) — Осуществляется дозвон
4 Incoming (MT call) — Входящий вызов
5 Waiting (MT call) — Состояние ожидания
<mode>Режим0 Голосовой вызов
1 Передача данных
2 Факс
9 Неизвестное
<mpty>Состояние конференц связи0 Вызов ни в одной из конференций
1 Вызов в конференции
<number>Номер телефона
<type>Тип номера129 Неизвестный тип номер
145 Международный номер

Сперва напишем прототип функции:

s32 RIL_GetCallState(s32 *callState);

Наша функция будет принимать в качестве параметра указатель на переменную, в которой будет сохраняться текущее состояние вызова, а возвращать будет ответ на запрос AT команды. Далее пишем определение функции и функцию обратного вызова для обработчика ответа на AT команду:

s32 RIL_GetCallState(s32 *callState)
{
    if (state == NULL)
        return QL_RET_ERR_INVALID_PARAMETER;

    return Ql_RIL_SendATCmd("AT+CLCC", 7, ATResponse_RIL_GetCallState, callState, 0);
}

static s32 ATResponse_RIL_GetCallState(char *line, u32 len, void *param)
{
    char *response;
    u8 stateOffset = 13;    // Смещение строки ответа для получения байта <stat>

    response = Ql_RIL_FindString(line, len, "+CLCC:");  // Найти строку +CLCC:
    if (response)
    {
        char state = *(line + stateOffset);
        switch (state)
        {
            case '0':
            (*(s32* )param) = CALL_ACTIVE;    // Записать в переменную состояние поднятой трубки
            break;
            ....
            // Таким же манером обрабатываем остальные состояния
            // Можно также здесь написать обработчик всех параметров ответа на команду AT+CLCC
        }
        return RIL_ATRSP_SUCCESS;
    } else {
        return RIL_ATRSP_FAILED;
    }

    return RIL_ATRSP_CONTINUE;
}

На этом все. Использовать данную функцию проще всего в отдельном потоке и только, когда осуществляется исходящий вызов. Алгоритм такой:
1. Создаем вызов;
2. Передаем в отдельный поток сообщение о начале вызова;
3. В потоке получаем сообщение и запускаем цикл обработки получения состояния вызова приблизительно такого содержания:

do
{
    RIL_GetCallState(&state);

    if (state == CALL_ACTIVE)
    {
        // Обработчик
    }

    Ql_Sleep(500);
} while (isThereOutgoingCall);

В обработчике можете записать любую логику, которую требуется запустить по состоянию поднятой трубки собеседником.

Я рассмотрел все, что касается OpenCPU RIL в данной статье, если у вас все же остались какие-то вопросы, то вы всегда можете писать в комментариях или обратиться к моим предыдущим статьям.

Делитесь данной статьей в соцсетях и подписывайтесь на мой ВК, Твиттер и заходите в наш чат Telegram.