Перейти к содержанию

WebServer на C++ с динамическим содержанием и Modbus TCP (QtWebApp, FreeMODBUS)

Подвернулась интересная задача (на собеседовании):

  • Три хоста в одной сети с веб интерфейсами – мастер и два слейва
  • Хосты общаются друг с другом на специфичном протоколе поверх TCP
  • Данные в веб-мордах отображают общение, управление
  • Памяти и вычислительной мощности очень мало – железки встраиваемые, крайне дешевые, но с Линуксом
  • Доступа во внешнюю сеть нет

Базовый проект

Собирается достаточно просто – тут описано все по шагам.

http://stefanfrings.de/qtwebapp/tutorial/index.html

Сокращенно перескажу то, что я использовал

Следуем по шагам ПРОПУСКАЕМ КУКИ И НАСТРОЙКУ СЕССИИ

А теперь добавим кое-что интересное, примеров использования которого я не особо нашел – AJAX в этой связке. То есть логично, что это будет работать (автор либы указывает на это в своем туториале), но вот реализацию было не подсмотреть нигде. Даже в исходниках “большого” проекта разработчика этой библиотеки – машины для варки пива(!).

Что есть AJAX двумя словами? Динамическое обновление. Или – это динамическое обновление содержания страницы данными с сервера без необходимости перезагружать страницу.

Раз в две секунды у нас отправляется GET-запрос на сервер по ноде “/parse_inc_data”. Ответ от сервера загружается в блок <textarea> с id=”demo”. Название test_msg_data здесь важно, т.к. именно это название будет фигурировать далее в коде бэкграунда при сборе JSON-строки ответа.

<!DOCTYPE html>
<html>
<body>

<div>
  <textarea readonly style="height:150px; width:70%; resize: none;" id="demo"></textarea></p>
</div>

<script>

function handle_data(obj){
    if (obj.test_msg_data != undefined && obj.test_msg_data.length > 0)
        document.getElementById("demo").value = obj.test_msg_data;
    }
}


setInterval(loadDoc, 1000);
function loadDoc() {
  const xhttp = new XMLHttpRequest();
  xhttp.onload = function() {

    var obj = JSON.parse(this.responseText);
    console.log(obj);
    handle_data(obj);

    }
    xhttp.open("GET", "/parse_inc_data");
    xhttp.send();
}


</script>

</body>
</html>

Коротко, со стороны Qt, в main.c пишем следующее

#include <httpserver/httplistener.h>
#include "requesthandler.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>


int main(int argc, char *argv[])
{
   //базовые настройки
    QSettings* settings=new QSettings(&app);
    settings->setValue("port","8080");
    settings->setValue("minThreads","4");
    settings->setValue("maxThreads","100");
    settings->setValue("cleanupInterval","60000");
    settings->setValue("readTimeout","60000");
    settings->setValue("maxRequestSize","16000");
    settings->setValue("maxMultiPartSize","10000000");

    //регистрируем слушателя наших запросов
    new HttpListener(settings,new RequestHandler(&app),&app);

    app.exec();
}

Создаем класс RequestMapper по образцу из библиотеки, но делаем свой ответ на запрос. Здесь мы используем счетчик, который отправляем в JSON-объект, сериализуем его в QByteArray и отправляем ответ.

...
RequestMapper::RequestMapper(QObject* parent)
    : HttpRequestHandler(parent) {
    // empty
}

void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
    QByteArray path=request.getPath();
    static int counter;
    //AJAX request
    if (path=="/parse_inc_data") {
        if (request.getMethod() == "GET"){
            //JSON data prepare
            QJsonObject object;

            //Show current status of icons
            object.insert("test_msg_data", counter++);
        
            //Serialize JSON to QByteArray and send it as answer
            QJsonDocument doc(object);
            QByteArray bytes = doc.toJson();
            response.write(bytes, true);
        }
    }
    else {
        response.setStatus(404,"Not found");
        response.write("The URL is wrong, no such document.",true);
    }
}
...

Собранное демо-приложение выглядит как-то так.

Как можем видеть – раз в секунду у нас обновляется счетчик. Таймаут обновления задается в setInterval(loadDoc, 1000) в .html-файле

Суть тестового задания и как оно было реализовано

Мастер-устройство (для каждого слейва):

  • Цветовой индикатор, управляемый слейвами
  • Поле ввода
  • Поле вывода
  • Чекбокс, меняющий цвет вывода на слейве
  • Кнопка, показывающая цветовой индикатор на слейве

Слейв-устройтсво:

  • Цветовой индикатор, управляемый мастером
  • Поле ввода
  • Поле вывода
  • Чекбокс, показывающий цветовой индикатор на мастере

Я решил делать все в одном проекте. Библиотека по условиям задания – FreeMODBUS.

С чекбоксами и кнопками достаточно просто – Modbus к ним приспособлен. Просто читаем или пишем в регистры-койлы. С сообщениями было интереснее – к ним протокол не особо приспособлен. Я решил сделать так:

  • Выделить определенный диапазон адресов под символы одного сообщения. У слйва их (диапазонов) 2 – от мастера и к мастеру. У мастера их 4. За очистку поля сообщения ответственна принимающая сторона.
  • Выделить к каждому диапазону адресов по одному статусному регистру. Флаг выставляется записывающей стороной и снимается принимающей. Флажок, по которому записывающая сторона может понять – были ли прочитаны данные, а принимающая – есть ли новые. После очистки данных с сообщением, приемник должен убрать флаг.
Упрощенно, как-то так

Были небольшие проблемы с библиотеками FreeMODBUS. Для реализации части Master были проблемы и инклудами, а для Slave – в открытом доступе не было. Нужно было делать запрос на “бесплатную покупку”, чтобы тебе выслали пример, но заработал он практически сразу.

Весь код приводить и расписывать не имеет смысла. В итоге, простенькое приложение будет выглядеть так:

Для того, чтобы собрать Slave или Master нужно определить одну из директив в файле web_server.pro

#can be defined as MODBUS_MASTER or MODBUS_SLAVE
DEFINES += MODBUS_MASTER

В директории web_server находятся исходные коды и проект для QT

В данном репозитории содержаться собранные проекты для windows:

  • release_final_master – TCP мастер
  • release_final_slave_1 – TCP подчиненный 1
  • release_final_slave_2 – TCP подчиненный 2

Файл etc/webapp1.ini в проектах позволяет провести конфигурацию запускаемого приложения, настроить порты, ip у подчиненных устройств и прочее

Примечание: Из мастера не сделать подчиненное устройство через конфигурационный файл(webapp1.ini) (верно и обратное)

Весь код находится по ссылке:

Comment

programel