Rai NewsSci, Soelden, gigante maschile: Pinheiro Braathen domina la prima manche. Diretta RaiNewsInquirer EntertainmentWinwyn Marquez, Mikee Quintos among new housemates of ‘PBB Collab 3.0’PunchPolice arrest tiler over death of hotel worker in AbujaThe Jerusalem PostArab Israeli voters are essential to Israel’s democratic future - opinionCNN TürkKaradeniz'de istilacı böcek kabusuna 'siber' kalkanInquirerFather, son shot dead over land dispute in QuezonESPNTransfer rumors, news: Arsenal, Man United, Liverpool track Bayern star Karl한겨레[사설] 독·일 합계도 넘어선 경상흑자, ‘반도체 이후’ 준비를Daily MaverickTIP-OFF AND BUST: Baobab trees seized in Durban ship warehouse probeZDF heuteEntdecken Sie das ZDF-NachrichtenstudioUOLComparecimento sobe na China continental, e Lula vence com 70,5% dos votosSCMP ChinaHow China’s regional leadership changes offer clues to the future of the Communist Party
The Daily Newsstand · Free, Always
Sunday, October 25, 2026

Декларативный подход к текстовым протоколам в Embedded на Си. Фреймворк TACT

Translate

Я занимаюсь разработкой встраиваемых систем уже несколько лет и не раз сталкивался с проблемой, когда было необходимо написать UART драйвер или систему для работы с каким-либо внешним модулем используя текстовый протокол, будь то протокол АТ команд или чего то подобного. Чаще всего это модули GNSS, модуль GSM, ESP или т.п. В таких случаях сложно ограничиться простой посылкой одной конкретной команды и ожидать что всё заработает, набор управляющих команд для таких модулей бесконечно огромен, и каждая из них часто требует отдельной обработки. Из-за чего постоянно приходилось городить бесконечные алгоритмы отправки разных последовательностей команд, приемки и парсинга их ответов, обработчиков ошибок.

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

Например ситуация: есть GSM модем, работает он через AT команды по UART. Ресурсов на устройстве очень мало, а через модем нужно выйти в сеть, подключиться к серверу и передать какие то данные. Что надо сделать понятно, но чтобы это получилось нужно послать примерно такую последовательность АТ команд:

AT+COPS? 
AT+COPS=0 
AT+CREG?
AT+CGATT?
AT+CIPMODE?
AT+CIPMODE=0
AT+CIPMUX?
AT+CIPMUX=0
AT+CIPSTATUS
AT+CSTT=…
AT+CIPSTATUS
AT+CIICR
AT+CIPSTATUS
AT+CIFSR
AT+CIPHEAD?
AT+CIPHEAD=1
AT+CIPSRIP
AT+CIPSRIP=0
AT+CIPSPRT=1
AT+CIPRXGET=0
AT+CIPSHOWTP
AT+CIPSHOWTP=1

И это только начало, в конечном результате их в разы больше. Большинство команд отвечают по разному, какие то возвращают данные, которые нужно обработать и использовать потом. А некоторые вообще в очередной раз могут вернуть ошибку и ее нужно просто пропустить. Итогом получается нужно писать свой обработчик и парсер под каждую команду, через колбэки или некую машину состояний их последовательно вызывать и пытаться как то организовать цепочку выполнения, а про обработку ошибок я вообще молчу.

Очередной раз занимаясь подобный, я задался вопросом, а почему бы не сделать один раз, а использовать для всего.
Из этой идеи и родился фреймворк TACT https://github.com/ferrero222/Text-Async-Core-Terminal
Изначально я хотел написать библиотеку под GSM модемы но быстро понял что реализация выходит за рамки. Что туда можно добавить поддержку любого текстового протокола и универсально слать команды.
Фреймворк сам берет на себя ответственность за передачу, конструкцию команды, принятие ответа, обработку формата ответа, его парсинга и даже сбор данных из ответа. Универсально, на чистом Си, без привязки к какой либо платформе и ОС, и без блокирующих операций. При этом затраты на оперативку и флеш минимальные. Размер используемой памяти можно установить самому, а статический оверхэд будет всего около 1кб. Достаточно лишь вызывать ядро раз в 10мс откуда угодно и на чем угодно.

Например, последовательность команд выше теперь можно описать в одном месте примерно так:

tact_item_t items[] = //[REQ][PREFIX][PARCE_TYPE][RPT][WAIT][STEPERROR][STEPOK][CB][FORMAT][...##VA_ARGS]
  {
    TACT_ITEM("AT+CIPMODE?"TACT_CMD_CRLF,        "+CIPMODE: 0", TACT_PARCE_SIMCOM,  1, 100, 1, 2, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPMODE=0"TACT_CMD_CRLF,                NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPMUX?"TACT_CMD_CRLF,          "+CIPMUX: 0", TACT_PARCE_SIMCOM,  1, 100, 1, 2, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPMUX=0"TACT_CMD_CRLF,                 NULL, TACT_PARCE_SIMCOM, 30, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSTATUS"TACT_CMD_CRLF,   "STATE: IP START", TACT_PARCE_SIMCOM,  1, 100, 1, 2, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CSTT=\"\",\"\",\"\""TACT_CMD_CRLF,      NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSTATUS"TACT_CMD_CRLF, "STATE: IP GPRSACT", TACT_PARCE_SIMCOM,  1, 100, 1, 2, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIICR"TACT_CMD_CRLF,                    NULL, TACT_PARCE_SIMCOM, 30, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSTATUS"TACT_CMD_CRLF, "STATE: IP GPRSACT", TACT_PARCE_SIMCOM,  3, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIFSR"TACT_CMD_CRLF,          TACT_CMD_FORCE, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPHEAD?"TACT_CMD_CRLF,        "+CIPHEAD: 1", TACT_PARCE_SIMCOM,  1, 100, 1, 2, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPHEAD=1"TACT_CMD_CRLF,                NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSRIP?"TACT_CMD_CRLF,        "+CIPSRIP: 0", TACT_PARCE_SIMCOM,  1, 100, 1, 2, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSRIP=0"TACT_CMD_CRLF,                NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSPRT=1"TACT_CMD_CRLF,                NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPRXGET=0"TACT_CMD_CRLF,               NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSHOWTP?"TACT_CMD_CRLF,    "+CIPSHOWTP: 1", TACT_PARCE_SIMCOM,  1, 100, 1, 0, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CIPSHOWTP=1"TACT_CMD_CRLF,              NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 0, NULL, NULL, TACT_NO_ARG),
  };
  if(!tact_entity_enqueue(ctx, items, sizeof(items)/sizeof(items[0]), cb, 0, meta)) return false;

Через фреймворк мы декларативно описываем набор команд, ожидаемый ответ, количество повторов, шаги перехода в случае успеха или ошибки и т.п. Больше не нужно самому писать парсер на каждую команду, обработчик, и самому обрабатывать переходы, всё это выполняется под капотом.

Можно даже через аргументы парсить данные из ответа, забирать их к себе через общий колбэк группы. Фреймворк сам локально и временно выделяет под данные место, передает их в колбек после получения, и сам очищает память обратно.

  tact_item_t items[] =
  {
   TACT_ITEM("AT+CGSN"TACT_CMD_CRLF, NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, "%15[^\x0d]", TACT_ARG(tact_mdl_rtd_identity_t, modem_imei)),
   TACT_ITEM("AT+CGMM"TACT_CMD_CRLF, NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, "%15[^\x0d]", TACT_ARG(tact_mdl_rtd_identity_t, modem_model)),
   TACT_ITEM("AT+CGMR"TACT_CMD_CRLF, NULL, TACT_PARCE_SIMCOM, 10, 100, 0, 1, NULL, "+CGMR: %29[^\x0d]", TACT_ARG(tact_mdl_rtd_identity_t, modem_revision))
  };
  tact_entity_enqueue(ctx, items, sizeof(items)/sizeof(items[0]), cb, sizeof(tact_mdl_rtd_identity_t), meta);

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

После реализации данного ядра команд, возник вопрос: а что если я хочу выполнять последовательно несколько групп команд, каждая со своим результатом и коллбеком. Строить из них полноценные цепочки выполнения, ветвления и циклы. Таким образом можно было бы описать впринципе всю работу с модулем посредством групп команд, в одном месте указать всю логику целиком. И опять же, декларативно.

Из этой идеи родилась вторая часть фреймворка - chain. Декларативное описание алгоритма выполнения групп команды. Для этого каждую группу команд мы должны обвернуть в стандартную функцию, например:

bool tact_mdl_gprs_init(tact_context_t* const ctx, const tact_entity_cb_t cb, const void* const param, void* const meta)
{
  (void)param;
  tact_item_t items[] = //[REQ][PREFIX][PARCE_TYPE][RPT][WAIT][STEPERROR][STEPOK][CB][FORMAT][...##VA_ARGS]
  {
    TACT_ITEM("AT+COPS?"TACT_CMD_CRLF,               "+COPS: 0", TACT_PARCE_SIMCOM, 5,  100, 1, 2, NULL, NULL,TACT_NO_ARG),
    TACT_ITEM("AT+COPS=0"TACT_CMD_CRLF,                    NULL, TACT_PARCE_SIMCOM, 5,  100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CREG?"TACT_CMD_CRLF,  "+CREG: 0,1|+CREG: 0,5", TACT_PARCE_SIMCOM, 30, 100, 0, 1, NULL, NULL, TACT_NO_ARG),
    TACT_ITEM("AT+CGATT?"TACT_CMD_CRLF,             "+CGATT: 1", TACT_PARCE_SIMCOM, 30, 100, 0, 0, NULL, NULL, TACT_NO_ARG),
  };
  if(!tact_entity_enqueue(ctx, items, sizeof(items)/sizeof(items[0]), cb, 0, meta)) return false;
  return true;
}

Получаем один готовый модуль цепочки выполнения. Можем сделать несколько таких модулей и создать из них цепочку - последовательность по типу:

chain_step_t tcp_steps[] = 
  {   
    //Main
    TACT_CHAIN("INIT_MODEM", "NEXT", "MODEM RESTART", tact_mdl_modem_init, NULL, NULL, NULL, 1),
    TACT_CHAIN("GPRS INIT", "NEXT", "GPRS DEINIT", tact_mdl_gprs_init, NULL, NULL, NULL, 1),
    TACT_CHAIN("SOCKET CONFIG", "NEXT", "GPRS INIT", tact_mdl_gprs_socket_config, NULL, NULL, NULL, 1),
    TACT_CHAIN("CONNECT TO SERVER", "NEXT", "SOCKET CONFIG", tact_mdl_gprs_socket_connect, NULL, &tact_server_connect, NULL, 1),
    TACT_CHAIN("GET RTD", "NEXT", "DISCONNECT FROM SERVER", tact_mdl_lte_rtd, tact_rtd_cb, NULL, NULL, 1),
    TACT_CHAIN_EXEC("CHECK RTD", "NEXT", "GET RTD", tact_rtd_check, NULL),
    
    TACT_CHAIN_EXEC("CREATE WIALON LOGIN", "NEXT", "DISCONNECT FROM SERVER", tact_server_data_wialon_login, NULL),
    TACT_CHAIN("SEND WIALON LOGIN", "NEXT", "DISCONNECT FROM SERVER", tact_mdl_gprs_socket_send_recieve, NULL, &tact_server_tx, NULL, 1),
    TACT_CHAIN_LOOP_START(0),
        TACT_CHAIN_EXEC("LOGIN TX", "LOGIN SEND RESULT", "NEXT", tact_mdl_gprs_stream_tx, &tact_server_tx),
    TACT_CHAIN_LOOP_END,
    TACT_CHAIN("LOGIN SEND RESULT", "NEXT", "DISCONNECT FROM SERVER", tact_mdl_gprs_socket_send_end, NULL, NULL, NULL, 1),
    TACT_CHAIN_LOOP_START(0),
        TACT_CHAIN_EXEC("LOGIN RX", "LOGIN CHECK REPLY", "NEXT", tact_mdl_gprs_stream_rx, &tact_server_rx),
    TACT_CHAIN_LOOP_END,
    TACT_CHAIN_EXEC("LOGIN CHECK REPLY", "NEXT", "DISCONNECT FROM SERVER", tact_server_answer_check, NULL),
    
    TACT_CHAIN_LOOP_START(10),
    TACT_CHAIN("GET RTD LOOP", "NEXT", "DISCONNECT FROM SERVER", tact_mdl_lte_rtd, tact_rtd_cb, NULL, NULL, 1),
      TACT_CHAIN_EXEC("CREATE WIALON DATA", "NEXT", "DISCONNECT FROM SERVER", tact_server_data_wialon_packet, NULL),
      TACT_CHAIN("SEND WIALON DATA", "NEXT", "DISCONNECT FROM SERVER", tact_mdl_gprs_socket_send_recieve, NULL, &tact_server_tx, NULL,1),
      TACT_CHAIN_LOOP_START(0),
          TACT_CHAIN_EXEC("DATA TX", "DATA SEND RESULT", "NEXT", tact_mdl_gprs_stream_tx, &tact_server_tx),
      TACT_CHAIN_LOOP_END,
      TACT_CHAIN("DATA SEND RESULT", "NEXT", "DISCONNECT FROM SERVER", tact_mdl_gprs_socket_send_end, NULL, NULL, NULL, 1),
      TACT_CHAIN_LOOP_START(0),
          TACT_CHAIN_EXEC("DATA RX", "DATA CHECK REPLY", "NEXT", tact_mdl_gprs_stream_rx, &tact_server_rx),
      TACT_CHAIN_LOOP_END,
      TACT_CHAIN_EXEC("DATA CHECK REPLY", "NEXT", "DISCONNECT FROM SERVER", tact_server_answer_check, NULL),
      TACT_CHAIN_DELAY(1000),
    TACT_CHAIN_LOOP_END,
    
    TACT_CHAIN_EXEC("WIALON DATA CLEAN", "STOP", "HARD RESET", tact_server_data_clean, NULL),
    
    //Error
    TACT_CHAIN("GPRS DEINIT", "GPRS INIT", "MODEM RESTART", tact_mdl_gprs_deinit, NULL, NULL, NULL, 1),
    TACT_CHAIN("DISCONNECT FROM SERVER", "CONNECT TO SERVER", "GPRS DEINIT", tact_mdl_gprs_socket_disconnect, NULL, NULL, NULL, 1),
    TACT_CHAIN("MODEM RESTART", "GPRS INIT", "MODEM RESTART", tact_mdl_modem_reset, NULL, NULL, NULL, 1),
    
    //Critical
    TACT_CHAIN_EXEC("HARD RESET", "STOP", "STOP", tact_hard_reset, NULL),

  };
  tact_chain_t* chain = tact_chain_create("TCP", tcp_steps, sizeof(tcp_steps)/sizeof(chain_step_t), &simcom_ctx);
  tact_chain_start(chain);

В примере у нас инициализируется модем, поднимается симкарта, получаются данные от модема, происходит регистрация в сети, создание TCP сокета к нужному серверу, составление пакета данных, передача и получение ответа от сервера. Каждый шаг выполнения может перейти к циклу исправления ошибок, работа с модулем может быть восстановлена или полностью остановлена в случае криитических ошибок. Всё это описываем в одном месте. Легко отладить и исправить ошибки.

Тут мы в одном месте описываем всю логику работы с модемом. Можем делать задержки, проверки условий, циклы, делать переходы назад, вперед, из цикла и в цикл, как угодно. Всё что надо это реализовать модули групп команд, описать их правильно и составить цепочку выполнения.

По сути, chain получился независимым, его можно использовать и обособленно от самих групп команд. Можно просто составить какую то свою логику выполнения чего либо на базе обычных функций, вариантов использования много.

В фреймворке уже организована поддержка simcom протокола стандарта 3GPP. Есть различные готовые модули для данных модемов. Изначально всё делалось чисто для себя, было протестировано и отлажено, а теперь работает на реальных устройствах, поэтому теперь захотелось поделиться с другими.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.