diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e12bd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build +.vscode \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6a51fac --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) + +# Главный проект +project(ScadaForDiesel + LANGUAGES CXX + DESCRIPTION "SCADA система стенда тестирования ДВС (дизельных двигателей)" + VERSION 1.0.0 +) + +# Глобальные настройки +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Директория для собранных файлов +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +# Backend библиотека +add_subdirectory(src/backend) + +# Информация о сборке +message(STATUS "") +message(STATUS "=== ScadaForDiesel ===") +message(STATUS "Version: ${PROJECT_VERSION}") +message(STATUS "CMake version: ${CMAKE_VERSION}") +message(STATUS "C++ Standard: ${CMAKE_CXX_STANDARD}") +message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") +message(STATUS "") diff --git a/Tz_backend.md b/Tz_backend.md new file mode 100644 index 0000000..2a97928 --- /dev/null +++ b/Tz_backend.md @@ -0,0 +1,326 @@ +# ТЕХНИЧЕСКОЕ ЗАДАНИЕ НА BACKEND (СВОДНОЕ, версия 2) + +## 1. Общие сведения и причина разработки + +Система создаётся для автоматизации процесса обкатки дизельных двигателей на испытательном стенде. +**Причина разработки:** необходимость управления режимами испытаний, контроля критических параметров, снижение влияния человеческого фактора, повышение наглядности и безопасности. +Реализуется в учебных целях с использованием моделируемых датчиков и исполнительных механизмов. + +Бэкенд выполняется как **библиотека**, работающая в отдельном потоке (`QThread`), и предоставляет API фронтенду исключительно через сигналы и слоты Qt. +Хранилище данных — CSV-файлы. + +## 2. Архитектура + +Бэкенд состоит из следующих модулей (каждый — `QObject`, живущий в рабочем потоке): + +- **Модуль связи с фронтом (BackendWorker)** – единственная точка входа для фронтенда. +- **Модуль управления процессом (StateMachine)** – конечный автомат этапов. +- **Модуль обработки данных (DataProcessor)** – проверка критических границ, генерация предупреждений и аварий. +- **Модуль связи с моделью (ModbusDataBridge)** – циклический опрос датчиков по Modbus TCP, запись управляющих сигналов. +- **Модуль хранения данных (DataStore)** – запись в CSV, чтение для отчётов. + +Обмен данными внутри потока – прямые вызовы методов; между потоком бэкенда и потоком фронтенда – сигналы/слоты Qt (`Qt::QueuedConnection`). +Все структуры данных зарегистрированы как метатипы Qt и передаются по значению через сигналы/слоты. + +## 3. Модули и их интерфейсы + +### 3.1. Модуль связи с фронтом (BackendWorker) + +Живёт в рабочем потоке. Владеет экземплярами всех остальных модулей, соединяет их сигналы и слоты. + + + +### 3.2. Модуль управления процессом (StateMachine) + +Хранит текущий этап, длительности, обрабатывает переходы. Не обязательно отдельный QObject, но для удобства может иметь сигнал смены этапа. + +**Слоты (вызываются BackendWorker'ом):** +- `void requestStart(Config config)` – войти в первый этап. +- `void requestNextStage()` – перейти к следующему этапу. +- `void requestAbort(QString reason)` – немедленно завершить испытание. + +**Сигналы (предложено автором):** +- `void stageChanged(int oldStage, int newStage)` – для логирования и обновления UI. + +**Состояния:** +`IDLE`, `COLD_CRANKING`, `START_AND_WARMUP`, `HOT_NO_LOAD`, `HOT_WITH_LOAD`, `COMPLETED`, `ABORTED`. + +### 3.3. Модуль обработки данных (DataProcessor) + +Принимает сырые измерения, сверяет с критическими порогами, формирует решение. + +**Слоты:** +- `void processFrame(SensorFrame frame)` *(из ТЗ обработки данных – приём текущего пакета)*. + +**Сигналы:** +- `void decisionReady(Decision decision)` *(из ТЗ обработки данных)*. +- `void warningRaised(WarningEvent event)` *(из ТЗ обработки данных)*. +- `void alarmRaised(AlarmEvent event)` *(из ТЗ обработки данных)*. +- `void controlNeeded(ActuatorCommand correction)` *(предложено автором)* – если требуется корректирующее воздействие. + +### 3.4. Модуль связи с моделью (IModbusDataBridge) + +Интерфейс реализует запросы к Modbus Server на чтение/запись данных. Интерфейс написан на Qt6.11. + +**Слоты:** +- `void onStartPolling()` – запуск таймера опроса. +- `void onStopPolling()` – остановка таймера. +- `void onReadSensors()` - обработка прямого запросо на чтение данных всех датчиков. +- `void onReadInfo()` - обработка прямого запроса на чтение настроек МК и состояний датчиков. +- `void onWriteCommand(ActuatorCommand cmd)` – запись управляющих регистров в модель *(из ТЗ ModelClient)*. + +**Сигналы:** +- `void modelInfoReady(const ModelInfo& data)` – после успешного чтения информации модели. +- `void sensorsDataReady(const SensorFrame& data)` – после успешного чтения регистров датчиков *(из ТЗ ModelClient)*. +- `void configurationError(QModbusDevice::Error type, const QString& reason)` – сигнал ошибки. +- `void connectionError(QModbusDevice::Error type, const QString& reason)` – сигнал ошибки. +- `void requestError(QModbusDevice::Error type, const QString& reason)` – сигнал ошибки. +- `void generalError(QModbusDevice::Error type, const QString& reason)` – сигнал ошибки. +- `void connectionLost()` – потеря соединения. +- `void connectionRestored()` – соединение восстановлено. + +**Конфиг (IModbusConfigBuilder):** +- Выдаёт готовый конфиг через `std::optional get() const`. +- Валидирует конфиг, при ошибках `get()` выдаст пустое значение. + +### 3.5. Модуль хранения данных (DataStore) + +Работает с CSV-файлами. Не имеет сигналов, только прямые методы и слоты для записи по сигналу. + +**Слоты (предложено автором):** +- `void writeRecord(MeasurementRecord record)` – дописать строку в CSV текущего испытания. +- `void writeEvent(EventRecord event)` – записать событие в журнал. + +**Прямые методы (вызываются из BackendWorker):** +- `QVector readRecords(quint64 runId)` +- `QVector readEvents(quint64 runId)` +- `Report buildReport(quint64 runId)` + +**Форматы файлов (для одного испытания `runId`):** +- `reports/run_.csv` – строки MeasurementRecord с заголовком. +- `reports/events_.csv` – строки EventRecord с заголовком. + +## 4. Структуры данных + +Все структуры имеют открытые поля и зарегистрированы как метатипы Qt. + +### ModbusConfig (схемы регистров могут быть другими) +```cpp +// Control registers mapping, read/write, 1-bit +struct CoilsRegistersScheme +{ + bool motorEnabled; +}; + +// Device status registers mapping, only read, 1-bit +struct DiscreteRegistersScheme +{ + bool fanAd; + bool fanBall; +}; + +// Settings and variables registers mapping, read/write, 16-bit +struct HoldingRegistersScheme +{ + double targetRpm; + double throttlePosition; + double brakeTorque; +}; + +// Sensor data registers mapping, only read, 16-bit +struct InputRegistersScheme +{ + double rpm, torque; + double dieselTemp, motorTemp, resistorTemp, dieselPressure; + double throttle, brakeTorque; +}; + +struct ModbusConfig +{ + QString host; + quint16 port = 1502; + int pollFrequencyMs = 1000; + int timeoutMs = 1000; + int retries = 3; + int unitId = 1; // we assume one modbus device, so unitId will be ignored + // Start address is 0 for each register type + CoilsRegistersScheme coils; + DiscreteRegistersScheme discrete; + InputRegistersScheme input; + HoldingRegistersScheme holding; +}; +``` + + +### Config (предложено автором) +```cpp +struct Config { + QVector stageDurationSec; + double targetRpmCold; + double throttleWarmup; + double throttleHotNoLoad; + double throttleHotLoad; + double brakeTorqueHotLoad; + + double maxRpmCold, maxRpmHot; + double maxMotorTemp, maxDieselTemp, maxResistorTemp; + double minDieselPressure, maxDieselPressure; +}; +``` + +### SensorFrame (из ТЗ ModelClient) +```cpp +struct SensorFrame { + qint64 runId; + int stage; + qint64 timestampMs; + double rpm, torque; + double dieselTemp, motorTemp, resistorTemp, dieselPressure; + double throttle, brakeTorque; +}; +``` + +### ModelConfig (из ТЗ ModelClient) +```cpp +struct ModelConfig +{ + bool motorEnabled; + double targetRpm; + double throttlePosition; + double brakeTorque; +}; +``` + +### ModelInfo (из ТЗ ModelClient) +```cpp +struct ModelInfo { + bool fanAd; + bool fanBall; + ModelConfig cfg; +}; +``` + +### Decision (из ТЗ обработки данных) +```cpp +enum class Status { OK, WARNING, ALARM }; + +struct Decision { + Status status; + QString message; + ActuatorCommand correction; // может быть пустым +}; +``` + +### WarningEvent (из ТЗ обработки данных) +```cpp +struct WarningEvent { + qint64 runId, timestampMs; + int stage; + QString parameter; + double currentValue, threshold; + QString description; +}; +``` + +### AlarmEvent (из ТЗ обработки данных) +```cpp +struct AlarmEvent { + qint64 runId, timestampMs; + int stage; + QString parameter; + double currentValue, criticalThreshold; + QString reason; +}; +``` + +### MeasurementRecord (предложено автором) +```cpp +struct MeasurementRecord { + qint64 runId; + int stage; + qint64 timestampMs; + double rpm, torque, dieselTemp, motorTemp, resistorTemp, dieselPressure; + double throttle, brakeTorque; + QString flags; // "OK", "WARNING" и т.п. +}; +``` + +### StageSummary (предложено автором) +```cpp +struct StageSummary { + int stageNum; + QString startTime, endTime; + double avgRpm, avgTorque, maxTemperature; + QString notes; +}; +``` + +### EventRecord (предложено автором) +```cpp +struct EventRecord { + qint64 runId, timestampMs; + int stage; + QString type; // "stage_change", "abort", "warning", "info" + QString message; +}; +``` + +### Report (предложено автором) +```cpp +struct Report { + qint64 runId; + QString startTime, endTime; + QString finalStatus; // "completed" или "aborted" + QVector stages; + QVector events; +}; +``` + +## 5. Точка сборки (main) + +Не является модулем. Выполняет: +- создание `QApplication`; +- загрузку начального `Config` (из файла или аргументов); +- создание объекта `BackendStarter` (или аналогичного), который создаёт `QThread`, экземпляр `BackendWorker`, перемещает его в поток, подключает сигналы `BackendWorker` к слотам фронтендного объекта; +- запуск потока. + +Фронтенд далее общается только через сигналы/слоты, передавая структуры напрямую. + +## 6. Схема взаимодействия (поток данных) + +### 6.1. Запуск испытания +Фронтенд → `onStart(config)`. +`BackendWorker`: +- создаёт запись в CSV (через `DataStore`); +- переводит `StateMachine` в `COLD_CRANKING`; +- формирует первичный `ActuatorCommand` и отправляет в `ModbusDataBridge::onWriteCommand`; +- запускает опрос: `ModbusDataBridge::onStartPolling()`. + +### 6.2. Цикл измерений (каждый тик таймера) +1. `ModbusDataBridge` успешно считывает регистры → `dataReady(frame)`. +2. `BackendWorker` направляет `frame` в `DataProcessor::processFrame(frame)`. +3. `DataProcessor` проверяет все критические границы и испускает один из: + - `decisionReady(decision)` – всегда, + - `warningRaised(event)` – если есть предупреждение, + - `alarmRaised(event)` – если есть авария, + - `controlNeeded(correction)` – если нужна корректировка. +4. `BackendWorker` (подписан на эти сигналы): + - при `warningRaised`: записывает событие в журнал CSV, отправляет `warningEvent` фронтенду; + - при `alarmRaised`: + - вызывает `StateMachine::requestAbort(reason)`, + - вызывает `ModbusDataBridge::onStopPolling()` и аварийный `onWriteCommand(стоп)`, + - записывает событие, + - отправляет `alarmEvent` фронтенду; + - при `controlNeeded`: передаёт `correction` в `ModbusDataBridge::onWriteCommand`; + - при любом `decisionReady`: сохраняет измерение в `DataStore::writeRecord(record)` (добавляя флаги из `decision.status`), отправляет `liveData(frame)` фронтенду. +5. Если аварии нет и длительность этапа истекла (контролирует `StateMachine` через внутренний таймер или по команде `requestNextStage`), `StateMachine` переходит к следующему этапу, сигнализирует `stageChanged`. `BackendWorker` формирует новые управляющие команды и обновляет `DataStore`. + +### 6.3. Завершение испытания +- При успешном завершении последнего этапа `StateMachine` переходит в `COMPLETED`. +- При аварии – в `ABORTED`. +- В обоих случаях `BackendWorker` останавливает опрос, закрывает CSV, вызывает `DataStore::buildReport(runId)`, получает `Report` и отправляет `processFinished(report)` фронтенду. + +### 6.4. Запросы от фронта вне активного испытания +- `onGetStatus` → `BackendWorker` отправляет `statusUpdated` с последним сохранённым состоянием. +- `onGetReport` → вызов `DataStore::buildReport`, ответ через `processFinished` (можно переиспользовать сигнал, передавая `report` и идентифицируя запрос). +- `onGetHistory` → чтение списка CSV-файлов в папке `reports`, возврат `historyReady(ids)`. diff --git a/include/backend/BackendGlobal.h b/include/backend/BackendGlobal.h new file mode 100644 index 0000000..6b89eee --- /dev/null +++ b/include/backend/BackendGlobal.h @@ -0,0 +1,8 @@ +#pragma once +#include + +#if defined(BACKEND_LIBRARY) +# define BACKEND_EXPORT Q_DECL_EXPORT +#else +# define BACKEND_EXPORT Q_DECL_IMPORT +#endif diff --git a/include/backend/DataTypes.h b/include/backend/DataTypes.h new file mode 100644 index 0000000..ffa8754 --- /dev/null +++ b/include/backend/DataTypes.h @@ -0,0 +1,255 @@ +// DataTypes.h (фрагмент, относящийся к DataProcessor) + +#ifndef DATATYPES_H +#define DATATYPES_H +#include +#include +#include +// InputRegisters offset (sensors data, only read operations) +namespace InputRegisters +{ + static constexpr qsizetype count = 50; + // Температура охлаждающей жидкости + static constexpr uint16_t T_cool = 0; + // Давление масла + static constexpr uint16_t P_oil = 4; + // Частота вращения ДВС в режиме притирки + static constexpr uint16_t omega_ICE_prir = 8; + // Частота вращения ДВС в режиме обкатки + static constexpr uint16_t omega_ICE_run = 12; + // Температура АД + static constexpr uint16_t T_AD = 16; + // Температура балластных резисторов + static constexpr uint16_t T_ballast = 20; + // Момент АД + static constexpr uint16_t M_AD = 24; + // Частота АД + static constexpr uint16_t f_AD = 28; + // Ревизия входных данных / версия источника + static constexpr uint16_t revision = 32; + // Ревизия источника входных данных + static constexpr uint16_t sourceInputRevision = 36; + // Время модели + static constexpr uint16_t modelTime = 40; + // Временная метка регистра Input Registers + static constexpr uint16_t timestamp_ir = 44; + // Состояние Input Registers + static constexpr uint16_t state_ir = 48; + // Код неисправности + static constexpr uint16_t faultCode = 49; +} + +// HoldingRegisters offset (settings and variables, read/write operations) +namespace HoldingRegisters +{ + static constexpr qsizetype count = 59; + // Максимально допустимая температура охлаждающей жидкости + static constexpr uint16_t T_cool_max = 0; + // Минимально допустимое давление масла + static constexpr uint16_t P_oil_min = 4; + // Максимально допустимое давление масла + static constexpr uint16_t P_oil_max = 8; + // Максимально допустимая частота вращения ДВС в режиме притирки + static constexpr uint16_t omega_ICE_max_prir = 12; + // Максимально допустимая частота вращения ДВС в режиме обкатки + static constexpr uint16_t omega_ICE_max_run = 16; + // Лишний регистр + static constexpr uint16_t rpm_max_lapping = 20; + // Лишний регистр + static constexpr uint16_t rpm_max_run = 24; + // Лишний регистр + static constexpr uint16_t target_brake_torque_nm = 28; + static constexpr uint16_t throttle_position = 32; + // Максимально допустимая температура АД + static constexpr uint16_t T_AD_max = 36; + // Максимально допустимая температура балластных резисторов + static constexpr uint16_t T_ballast_max = 40; + // Входная частота для АД / задание частоты АД + static constexpr uint16_t f_AD_Input = 44; + // Целевая механическая нагрузка / момент АД + static constexpr uint16_t M_AD_target = 48; + // Версия (ревизия) настроек holding-регистров + static constexpr uint16_t revision_h = 52; + // Команда на симуляцию + static constexpr uint16_t simulationCommand = 56; + // Запрос на симуляцию + static constexpr uint16_t simulationRequest = 57; + // Режим симуляции + static constexpr uint16_t simulationMode = 58; +} + +// Coils offset (control registers, read/write operations) +namespace CoilsRegisters +{ + static constexpr uint16_t count = 3; + static constexpr uint16_t fan_ICE = 0; + static constexpr uint16_t fan_AD = 1; + static constexpr uint16_t fan_ballast = 2; +} + +// Discrete Inputs offset (device status, only read operations) +namespace DiscreteRegisters +{ + static constexpr uint16_t count = 2; + static constexpr uint16_t hasFault = 0; + static constexpr uint16_t hasLimitViolations = 1; +} + +//Структра для задания конфигурации Modbus-клиента +struct ModbusConfig +{ + //Ip-адрес Modbus-сервера + QString host = "127.0.0.1"; + //Порт Modbus-сервера + quint16 port = 1502; + int pollFrequencyMs = 1000; + int timeoutMs = 1000; + int retries = 3; + int unitId = 1; // we assume one modbus device, so unitId will be ignored +}; + +//Снимок датчиков +struct SensorFrame +{ + //Температура ДВС(температура охлаждающей жидкости) + double dieselTemp; + //Температура АД + double motorTemp; + //Температура балластных резисторов + double resistorTemp; + //Давление масла + double dieselPressure; + //Момент АД + double torque; + //Частота вращения ДВС + double rpm; + //Метка времени в UNIX-формате + qint64 timestampMs; + // Этап диагностики + int stage; +}; +Q_DECLARE_METATYPE(SensorFrame) + +// лимиты, передаются в конструктор DataProcessor и в IModbusBridge для записи в модель +struct ModelConfig +{ + //Максимальная температура ДВС(температура охлаждающей жидкости) + double maxDieselTemp; + //Максимальная температура АД + double maxMotorTemp; + //Максимальная температура балластных резисторов + double maxResistorTemp; + //Максимальное давление масла в ДВС + double maxDieselPressure; + //Минимальное давление масла в ДВС + double minDieselPressure; + //Общая частота оборотов в режиме притирки + int maxRpmPrir; + //Общая частота оборотов в режиме обкатки + int maxRpmRun; + //Частота АД + double freqAD; + //Момент АД + double momentAD; +}; +Q_DECLARE_METATYPE(ModelConfig) + + +//Команды на симуляцию +enum class SimulationCommand : uint8_t +{ + None, + Start, + Stop, + Reset, + EmergencyStop +}; + +//Запрос на симуляцию +enum class SimulationRequest : uint8_t +{ + None, + ReadCurrentState, + StepAndRead +}; + +//Режим симуляции +enum class SimulationMode : uint8_t +{ + ColdRun, + StartWarmup, + HotNoLoad, + HotLoad +}; + +// тип управляющего воздействия (это требует уточнения) +enum class ControlType +{ + None, + SimulationCommand, // Команда на симуляцию (simulationCommand) + SimulationRequest, // Запрос на симуляцию (simulationRequest) + SimulationMode, // Режим симуляции (simulationMode) + Fan_ICE, + Fan_AD, + Fan_Ballast +}; + +// одно управляющее воздействие (тип + значение) +struct ModelControl +{ + ControlType type = ControlType::None; + uint8_t value = 0; +}; +Q_DECLARE_METATYPE(ModelControl) + +// диагностическое состояние модели (добавлены предаварийные) +enum class DiagState +{ + IDLE, + Ok, + PreWarn_RpmHigh, + PreWarn_DieselTempHigh, + PreWarn_MotorTempHigh, + PreWarn_ResistorHigh, + PreWarn_PressureHigh, + PreWarn_PressureLow, + Alarm_RpmOverspeed, + Alarm_DieselOverheat, + Alarm_MotorOverheat, + Alarm_ResistorOverheat, + Alarm_PressureOver, + Alarm_PressureUnder, + Alarm_Generic +}; + +//Структра для управления моделью фронтом +struct FrontControl +{ + //Время обкатки ДВС в режиме притирки(в минутах) + unsigned int timePrir; + //Время горячей обкатки ДВС(в минутах) + unsigned int timeHot; + //Время горячей обкатки ДВС с нагрузкой(в минутах) + unsigned int timeHotWithLoad; + //Структура для задания минимальных/максимальных значений в модели + ModelConfig config; +}; + +// вектор управляющих воздействий + диагностика +struct Decision +{ + QVector controls; + DiagState state = DiagState::Ok; + QString reason; // причина (для последующей записи в БД, если надо записывать, требует уточнения) +}; +Q_DECLARE_METATYPE(Decision) + +//Структра для DataStore +struct Data +{ + //Снимок датчиков + SensorFrame frame; + //Этап эксперимента + DiagState state; +}; +#endif // DATATYPES_H diff --git a/include/backend/backend_worker/backendcommunicator.h b/include/backend/backend_worker/backendcommunicator.h new file mode 100644 index 0000000..ef1b436 --- /dev/null +++ b/include/backend/backend_worker/backendcommunicator.h @@ -0,0 +1,44 @@ +#ifndef BACKENDCOMMUNICATOR_H +#define BACKENDCOMMUNICATOR_H + +#include +#include "backend/DataTypes.h" + +class BackendCommunicator : public QObject +{ + Q_OBJECT +public: + explicit BackendCommunicator(QObject *parent = nullptr); + + //Метод для отправки нового снимка датчиков фронту + void SendSensorFrameToFrontend(SensorFrame& sensorFrame); + //Метод для отправки информации фронту об аварийной остановке + void SendEmergencyStopInfoToFrontend(); + //Метод для отправки фидбека фронту на операцию + void SendFeedbackToFrontend(DiagState state); + //Метод для отправки данных фронту + void SendDataToFrontend(QVector& data); + //Метод для отправки предупреждений фронту + void SendWarnToFrontend(DiagState state); + //Метод для отправки сообщения о завершенном этапе + void SendStageCompleteInfoToFrontend(); + +signals: + //Сигналы для отправки(внутренние) + void SendedSensorFrame(SensorFrame& sensorFrame); + void SendedEmergencyStopInfo(); + void SendedFeedback(DiagState state); + void SendedData(QVector& data); + void SendedWarnToFrontend(DiagState state); + void SendedStageCompleteInfoToFrontend(); + + //Сигналы для получения + //Сигнал на приход запроса на изменения этапа эксперимента + void ReceivedFrontControl(FrontControl control); + //Сигнал на запуск обкатки + void ReceivedStartEngine(); + //Сигнал на остановку обкатки + void ReceivedStopEngine(); +}; + +#endif // BACKENDCOMMUNICATOR_H diff --git a/include/backend/backend_worker/backendworker.h b/include/backend/backend_worker/backendworker.h new file mode 100644 index 0000000..f7bbecc --- /dev/null +++ b/include/backend/backend_worker/backendworker.h @@ -0,0 +1,54 @@ +#ifndef BACKENDWORKER_H +#define BACKENDWORKER_H + +#include +#include +#include "backend/state_machine/state_machine.h" +#include "backend/DataTypes.h" + +//Класс взаимодействия с фронтом +class BackendWorker : public QObject +{ + Q_OBJECT +public: + explicit BackendWorker(QObject *parent = nullptr); + ~BackendWorker(); + + //Запуск бэкэнда + void Run(); + //Остановка бэкэнда + void Stop(); + + //Метод для отправки параметров модели бэкэнду + void SendFrontControlToBackend(FrontControl& control); + //Запустить обкатку + void StartEngine(); + //Остановить обкатку + void StopEngine(); + +signals: + //Сигналы для получения(для фронта) + //Сигнал на приход нового снимка датчиков + void ReceivedSensorFrame(SensorFrame sensorFrame); + //Сигнал на приход информации об аварийной остановке + void ReceivedEmergencyStopInfo(); + //Сигнал на приход фидбека на изменение этапа + void ReceivedFeedback(DiagState state); + //Сигнал на приход данных + void ReceivedData(QVector& data); + //Сигнал на приход предупреждений в работе двигателя + void ReceivedWarn(DiagState state); + //Сигнал при окончании выполнения этапа обкатки + void ReceivedStageCompleteInfo(); + + //Сигналы для отправки(внутренние) + void SendedFrontControlToBackend(FrontControl control); + void StartedEngine(); + void StopedEngine(); + +private: + QThread m_machineThread; + StateMachine* m_machine; +}; + +#endif // BACKENDWORKER_H diff --git a/include/backend/config_data/config_data.h b/include/backend/config_data/config_data.h new file mode 100644 index 0000000..ed0625a --- /dev/null +++ b/include/backend/config_data/config_data.h @@ -0,0 +1,29 @@ +#ifndef CONFIG_DATA_H +#define CONFIG_DATA_H + +#include +#include +#include +#include +#include "backend/DataTypes.h" + +class ConfigData : public QObject +{ +public: + ConfigData(); + ModbusConfig LoadConfig(); + +private: + const QString m_HOST = "host"; + const QString m_PORT = "port"; + const QString m_POLL_FREQ = "poll_freq_ms"; + const QString m_TIMEOUT = "timeout_ms"; + const QString m_RETRIES = "retries"; + const QString m_UNIT_ID = "unit_id"; + + const QString m_FILENAME = "configBackend.txt"; +}; + + + +#endif //Config_data H diff --git a/include/backend/data_processing/DataProcessor.h b/include/backend/data_processing/DataProcessor.h new file mode 100644 index 0000000..9800621 --- /dev/null +++ b/include/backend/data_processing/DataProcessor.h @@ -0,0 +1,50 @@ +#ifndef DATAPROCESSOR_H +#define DATAPROCESSOR_H + +#include +#include +#include "backend/DataTypes.h" + +/** + * Модуль обработки данных. + * версия 4 + * Принимает SensorFrame, сравнивает с порогами из ModelConfig (с учётом этапа), + * формирует Decision: вектор управляющих воздействий + DiagState + reason. + * + * + * используем только существующие ControlType + * (Throttle, BrakeTorque, TargetRpm, MotorEnable, EmergencyStop). + * Вентиляторы охлаждения управляются через MotorEnable/коды режимов + * не могут — поэтому при перегревах формируем только DiagState + * и текстовую причину; включением вентиляторов будет заниматься тот, + * кто реализует applyControls (в Modbus у нас есть CoilsRegisters::fan_*). + * - дублируем выдачу Decision двумя путями: + * 1) processFrame возвращает Decision (если кто-то захочет использовать); + * 2) сигнал decisionReady(Decision) + * закомментированный connect в state_machine.cpp. + */ +class DataProcessor : public QObject +{ + Q_OBJECT +public: + explicit DataProcessor(const ModelConfig &config, QObject *parent = nullptr); + Decision processFrame(const SensorFrame &frame); +public slots: + void onStageChanged(int newStage); + void onConfigChanged(ModelConfig config); + void reset(); + +signals: + void decisionReady(const Decision &decision); + +private: + bool isParamActiveOnStage(const QString ¶m, int stage) const; + static QVector makeStopControls(); + +private: + ModelConfig m_config; + int m_currentStage = 0; + bool m_alarmLatched = false; +}; + +#endif // DATAPROCESSOR_H diff --git a/include/backend/data_store/DataStore.h b/include/backend/data_store/DataStore.h new file mode 100644 index 0000000..70bae47 --- /dev/null +++ b/include/backend/data_store/DataStore.h @@ -0,0 +1,83 @@ +#ifndef DATASTORE_H +#define DATASTORE_H + +#include +#include + +#include + +#include "backend/DataTypes.h" // Data + +// Low-level layer +class Connector +{ +public: + Connector(std::string path, size_t record_size); + virtual ~Connector() noexcept = default; + + virtual int64_t write(const std::string &data) = 0; + virtual std::optional read(int64_t id) const = 0; + virtual bool update(int64_t id, const std::string &data) = 0; + virtual bool remove(int64_t id) = 0; + + size_t getSize() const noexcept; + size_t getRecordSize() const noexcept; + const std::string &getPath() const noexcept; + +protected: + void setSize(size_t size) noexcept; + +private: + std::string path_; + size_t size_; + size_t record_size_; +}; + +class FileConnector : public Connector +{ +public: + FileConnector(std::string path, size_t record_size); // Необходимо определить макрос для размера одной записи (128 байт хватит); #define RECORD_SIZE 128 + ~FileConnector() noexcept override = default; + + int64_t write(const std::string &data) override; + std::optional read(int64_t id) const override; + bool update(int64_t id, const std::string &data) override; + bool remove(int64_t id) override; + +private: + static constexpr char RECORD_DELIMITER = '\n'; + + size_t getFullRecordSize() const noexcept; + bool isValidId(int64_t id) const noexcept; + std::streampos getOffset(int64_t id) const noexcept; + std::string normalizeRecord(const std::string &data) const; + + bool seekRead(std::fstream &file, std::streampos offset) const; + bool seekWrite(std::fstream &file, std::streampos offset) const; + bool readRecord(std::fstream &file, std::streampos offset, std::string &buffer) const; + bool writeRecord(std::fstream &file, std::streampos offset, const std::string &data); +}; + +// Qt layer +class DataStore : public QObject +{ + Q_OBJECT + +public: + DataStore(Connector *connector, QObject *parent = nullptr); + QVector readData(qint64 id) const; // В случае ошибок возвращает пустой вектор + QVector readAllData() const; // В случае ошибок возвращает пустой вектор + +public slots: + bool writeData(const Data &record); + +private: + QString serializeData(qint64 id, const Data &record, int precision = 3) const; + bool deserializeData(const QString &line, Data &data) const; + qint64 generateId() noexcept; + +private: + Connector *connector_; +}; + +#endif // DATASTORE_H \ No newline at end of file diff --git a/include/backend/modbus_client/IModbusBridge.h b/include/backend/modbus_client/IModbusBridge.h new file mode 100644 index 0000000..c654287 --- /dev/null +++ b/include/backend/modbus_client/IModbusBridge.h @@ -0,0 +1,55 @@ +#pragma once +#include +#include +#include "backend/DataTypes.h" +#include "backend/BackendGlobal.h" + +class BACKEND_EXPORT IModbusBridge : public QObject +{ + Q_OBJECT +public: + explicit IModbusBridge(const ModbusConfig& cfg, QObject* parent = nullptr); + virtual ~IModbusBridge() = default; +public slots: + // Starting a scheduled data retrieval from the sensor using a timer + virtual void startPolling() = 0; + // Stopping a scheduled data retrieval from the sensor + virtual void stopPolling() = 0; + // Handling direct call to read sensor data + virtual void onReadSensors() = 0; + // Handling direct call to read the model info (holding registers or in other words model config) + virtual void onReadInfo() = 0; + // Handling direct call to write new model configuration at startup + virtual void onWriteConfig(const ModelConfig& cmd) = 0; + // Handling direct call to write controlling command at runtime + virtual void onWriteDecision(const Decision& decision) = 0; +signals: + // A response to the sensor data request has been received + void sensorsDataReady(const SensorFrame& data); + // A response to the model info request has been received + void modelInfoReady(const ModelConfig& data); + // Error signals + /** + * @brief Emitted when configuration fails during startup/setup. + * Note: config validation should be handled by the config builder, + * so this error would not even happen. + */ + void configurationError(const QString& reason); + /** + * @brief Emitted when physical connection fails or is lost. + * Groups QModbusDevice errors: ConnectionError. + */ + void connectionError(const QString& reason); + /** + * @brief Emitted when a read/write request fails. + * Occurs during: Polling requests, direct read/write requests. + */ + void requestError(const QString& reason); + /** + * @brief Any error not fitting above categories + */ + void generalError(const QString& reason); + // Transition states + void connectionLost(); + void connectionRestored(); +}; diff --git a/include/backend/modbus_client/MockModbusBridge.h b/include/backend/modbus_client/MockModbusBridge.h new file mode 100644 index 0000000..420d144 --- /dev/null +++ b/include/backend/modbus_client/MockModbusBridge.h @@ -0,0 +1,21 @@ +#pragma once +#include "IModbusBridge.h" +#include "backend/DataTypes.h" +#include + +class MockModbusBridge : public IModbusBridge +{ + Q_OBJECT +public: + explicit MockModbusBridge(const ModbusConfig& cfg, QObject* parent = nullptr); +public slots: + void startPolling() override; + void stopPolling() override; + void onReadSensors() override; + void onReadInfo() override; + void onWriteConfig(const ModelConfig& cmd) override; + void onWriteDecision(const Decision& decision) override; +private: + ModbusConfig m_cfg; + QTimer m_pollTimer; +}; diff --git a/include/backend/modbus_client/QtModbusBridge.h b/include/backend/modbus_client/QtModbusBridge.h new file mode 100644 index 0000000..78455b5 --- /dev/null +++ b/include/backend/modbus_client/QtModbusBridge.h @@ -0,0 +1,44 @@ +#pragma once +#include "IModbusBridge.h" +#include "backend/DataTypes.h" +#include "backend/modbus_client/RegisterConverter.h" +#include +#include +#include + +struct RegisterBlock +{ + QModbusDataUnit::RegisterType type; + quint16 startAddress; + QVector values; +}; + +class BACKEND_EXPORT QtModbusBridge : public IModbusBridge +{ + Q_OBJECT +public: + explicit QtModbusBridge(const ModbusConfig& cfg, QObject* parent = nullptr); + ~QtModbusBridge() override; +public slots: + void startPolling() override; + void stopPolling() override; + void onReadSensors() override; + void onReadInfo() override; + void onWriteConfig(const ModelConfig& cmd) override; + void onWriteDecision(const Decision& decision) override; +private slots: + void onStateChange(QModbusDevice::State state); + void onErrorOccured(); +private: + void parseSensorsResponse(const QVector& values); + void parseInfoResponse(const QVector& values); + std::optional> extractValues(QModbusReply* reply, qsizetype expectedSize); + //OPTIMIZE: move sorting elsewhere and receive regs by reference if encountered speed issues + void writeRegisterVector(QVector regs); +private: + ModbusConfig m_cfg; + QModbusTcpClient m_client; + QTimer m_pollTimer; + RegisterConverter m_reg_converter; + QModbusDevice::State m_prevState = QModbusDevice::UnconnectedState; +}; diff --git a/include/backend/modbus_client/RegisterConverter.h b/include/backend/modbus_client/RegisterConverter.h new file mode 100644 index 0000000..2bd34fd --- /dev/null +++ b/include/backend/modbus_client/RegisterConverter.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include +#include + +class RegisterConverter +{ +public: + double fromRegisterWordDouble(const quint16* value); + qint64 fromRegisterWordQint(const quint16* value); + int fromRegisterWordInt(const quint16* value); + template + QVector toRegisterWords(const ValueT& value); +private: + quint64 fromRegisterWordRawValue(const quint16* value, qsizetype count); +}; + +template +QVector RegisterConverter::toRegisterWords(const ValueT& value) +{ + static_assert(std::is_trivially_copyable_v); + if (sizeof(ValueT) % sizeof(quint16) != 0) + { + qWarning() << "Value type must be divisible by register word"; + return {}; + } + auto bits = std::bit_cast>(value); + return QVector(bits.begin(), bits.end()); +} diff --git a/include/backend/state_machine/state_machine.h b/include/backend/state_machine/state_machine.h new file mode 100644 index 0000000..a5eb734 --- /dev/null +++ b/include/backend/state_machine/state_machine.h @@ -0,0 +1,84 @@ +#ifndef STATE_MACHINE_H +#define STATE_MACHINE_H + +#include +#include + +#include "backend/DataTypes.h" +#include "backend/config_data/config_data.h" + +#include "backend/backend_worker/backendcommunicator.h" +#include "backend/modbus_client/IModbusBridge.h" +#include "backend/data_processing/DataProcessor.h" + +class BackendWorker; +class IModbusBridge; +class DataStore; +class DataProcessor; +class FileConnector; + +class StateMachine : public QObject +{ + Q_OBJECT +public: + explicit StateMachine(BackendWorker* backendWorker, QObject* parent = nullptr); + ~StateMachine(); + + void start(); + void stop(); + + void requestNextStage(); + void requestAbort(const QString& reason = QString()); + + DiagState currentState() const; + int currentStageIndex() const; + +signals: + void stageChanged(int oldStage, int newStage); + void finished(DiagState finalState); + +private slots: + void pollModbus(); + void onSensorsDataReady(const SensorFrame& frame); + void onDecisionReady(const Decision& decision); + + // Ошибки Modbus + void onModbusConfigurationError(const QString& reason); + void onModbusConnectionError(const QString& reason); + void onModbusRequestError(const QString& reason); + void onModbusConnectionLost(); + void onModbusConnectionRestored(); + + // Команды фронта + void onReceivedFrontControl(); + void onReceivedModelConfig(const ModelConfig& config); + +private: + void transitionTo(DiagState newState); + void applyControls(const QVector& controls); + + // Коммуникатор + BackendCommunicator* m_communicator; + + // Модули + IModbusBridge* m_modbusBridge; + DataStore* m_dataStore; + DataProcessor* m_dataProcessor; + + //Коннектор + FileConnector* m_Connector; + + // Таймеры + QTimer* m_pollTimer; + QTimer* m_stageTimer; + + // Состояние + DiagState m_state = DiagState::IDLE; + ModelConfig m_config; + SensorFrame m_currentSensorFrame; + int m_currentStageIndex = -1; + int m_currentRunId = 0; // для связывания записей в datastore + qint64 m_runStartTime = 0; // для репортов +}; + +#endif // STATE_MACHINE_H \ No newline at end of file diff --git a/src/backend/CMakeLists.txt b/src/backend/CMakeLists.txt new file mode 100644 index 0000000..14d66d6 --- /dev/null +++ b/src/backend/CMakeLists.txt @@ -0,0 +1,133 @@ +cmake_minimum_required(VERSION 3.16) + +# Настройки проекта +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# Зависимости +find_package(Qt6 REQUIRED COMPONENTS + Core + Network + Test + SerialBus +) + +# Исходники +set(BACKEND_SOURCES + backend_worker/backendworker.cpp + backend_worker/backendcommunicator.cpp + config_data/config_data.cpp + data_processing/DataProcessor.cpp + data_store/DataStore.cpp + modbus_client/IModbusBridge.cpp + modbus_client/QtModbusBridge.cpp + modbus_client/MockModbusBridge.cpp + modbus_client/RegisterConverter.cpp + state_machine/state_machine.cpp +) + +# Заголовки +set(BACKEND_HEADERS + ${PROJECT_SOURCE_DIR}/include/backend/backend_worker/backendworker.h + ${PROJECT_SOURCE_DIR}/include/backend/backend_worker/backendcommunicator.h + ${PROJECT_SOURCE_DIR}/include/backend/data_processing/DataProcessor.h + ${PROJECT_SOURCE_DIR}/include/backend/data_store/DataStore.h + ${PROJECT_SOURCE_DIR}/include/backend/DataTypes.h + ${PROJECT_SOURCE_DIR}/include/backend/BackendGlobal.h + ${PROJECT_SOURCE_DIR}/include/backend/config_data/config_data.h + ${PROJECT_SOURCE_DIR}/include/backend/modbus_client/IModbusBridge.h + ${PROJECT_SOURCE_DIR}/include/backend/modbus_client/QtModbusBridge.h + ${PROJECT_SOURCE_DIR}/include/backend/modbus_client/MockModbusBridge.h + ${PROJECT_SOURCE_DIR}/include/backend/modbus_client/RegisterConverter.h + ${PROJECT_SOURCE_DIR}/include/backend/state_machine/state_machine.h +) + +# Библиотека +add_library(backend_lib SHARED) + +target_compile_definitions(backend_lib PRIVATE BACKEND_LIBRARY) + +target_sources(backend_lib + PRIVATE + ${BACKEND_SOURCES} + ${BACKEND_HEADERS} +) + +# Include paths для компиляции +target_include_directories(backend_lib PUBLIC + ${PROJECT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Зависимости +target_link_libraries(backend_lib + PUBLIC + Qt6::Core + Qt6::Network + Qt6::Test + Qt6::SerialBus +) + +# Группировка файлов для QtCreator +# Это убирает попадание хедеров в +source_group(TREE "${PROJECT_SOURCE_DIR}/include" PREFIX "include" FILES ${BACKEND_HEADERS}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "src" FILES ${BACKEND_SOURCES}) + +# Unit Tests +# enable_testing() + +# # Макрос для создания тестов +# macro(add_backend_test TEST_NAME) +# add_executable(${TEST_NAME} ${ARGN}) + +# target_include_directories(${TEST_NAME} PRIVATE +# ${PROJECT_SOURCE_DIR}/include +# ${CMAKE_CURRENT_SOURCE_DIR} +# ) + +# target_link_libraries(${TEST_NAME} PRIVATE +# Qt6::Core +# Qt6::Test +# Qt6::Network +# Qt6::SerialBus +# backend_lib +# ) + +# add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) +# endmacro() + +# # Добавляем тесты +# add_backend_test(test_backend_worker +# unit_tests/backend_worker_test/backend_worker_test.cpp +# unit_tests/backend_worker_test/backend_worker_test.h +# ) + +# add_backend_test(test_data_processor +# unit_tests/data_processor_test/data_processor_test.cpp +# unit_tests/data_processor_test/data_processor_test.h +# ) + +# add_backend_test(test_data_store +# unit_tests/data_store_test/data_store_test.cpp +# unit_tests/data_store_test/data_store_test.h +# ) + +# add_backend_test(test_modbus_data_bridge +# unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.cpp +# unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.h +# ) + +# add_backend_test(test_state_machine +# unit_tests/state_machine_test/state_machine_test.cpp +# unit_tests/state_machine_test/state_machine_test.h +# ) + +# Вывод информации о сборке +message(STATUS "Backend Library: backend_lib") +message(STATUS "C++ Standard: ${CMAKE_CXX_STANDARD}") +message(STATUS "Qt6 Version: ${Qt6Core_VERSION}") + +message(STATUS "Tests enabled") \ No newline at end of file diff --git a/src/backend/backend_worker/backendcommunicator.cpp b/src/backend/backend_worker/backendcommunicator.cpp new file mode 100644 index 0000000..8df85c3 --- /dev/null +++ b/src/backend/backend_worker/backendcommunicator.cpp @@ -0,0 +1,34 @@ +#include "backend/backend_worker/backendcommunicator.h" + +BackendCommunicator::BackendCommunicator(QObject *parent) + : QObject{parent} {} + +void BackendCommunicator::SendSensorFrameToFrontend(SensorFrame& sensorFrame) +{ + emit SendedSensorFrame(sensorFrame); +} + +void BackendCommunicator::SendEmergencyStopInfoToFrontend() +{ + emit SendedEmergencyStopInfo(); +} + +void BackendCommunicator::SendFeedbackToFrontend(DiagState state) +{ + emit SendedFeedback(state); +} + +void BackendCommunicator::SendDataToFrontend(QVector& data) +{ + emit SendedData(data); +} + +void BackendCommunicator::SendWarnToFrontend(DiagState state) +{ + emit SendedWarnToFrontend(state); +} + +void BackendCommunicator::SendStageCompleteInfoToFrontend() +{ + emit SendedStageCompleteInfoToFrontend(); +} diff --git a/src/backend/backend_worker/backendworker.cpp b/src/backend/backend_worker/backendworker.cpp new file mode 100644 index 0000000..43a2348 --- /dev/null +++ b/src/backend/backend_worker/backendworker.cpp @@ -0,0 +1,46 @@ +#include "backend/backend_worker/backendworker.h" +#include "backend/state_machine/state_machine.h" + +BackendWorker::BackendWorker(QObject* parent) : + m_machine(new StateMachine(this, nullptr)), QObject{parent} +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + connect(&m_machineThread, &QThread::started, m_machine, &StateMachine::start, Qt::QueuedConnection); + connect(&m_machineThread, &QThread::finished, m_machine, &StateMachine::stop, Qt::QueuedConnection); +} + +void BackendWorker::SendFrontControlToBackend(FrontControl& control) +{ + emit SendedFrontControlToBackend(control); +} + +void BackendWorker::StartEngine() +{ + emit StartedEngine(); +} + +void BackendWorker::StopEngine() +{ + emit StopedEngine(); +} + +void BackendWorker::Run() +{ + m_machine->moveToThread(&m_machineThread); + m_machineThread.start(); +} + +void BackendWorker::Stop() +{ + m_machineThread.quit(); +} + +BackendWorker::~BackendWorker() +{ + if(m_machineThread.isRunning()) + m_machineThread.quit(); + m_machineThread.wait(); + m_machine->deleteLater(); +} diff --git a/src/backend/config_data/config_data.cpp b/src/backend/config_data/config_data.cpp new file mode 100644 index 0000000..cccc0f6 --- /dev/null +++ b/src/backend/config_data/config_data.cpp @@ -0,0 +1,61 @@ +#include "backend/config_data/config_data.h" + +ConfigData::ConfigData(){} + +ModbusConfig ConfigData::LoadConfig() +{ + QFile file(m_FILENAME); + if(!file.exists()) + { + qInfo("Файл конфигурации бэкэнда отсутствует. Будут загружены параметры по умолчанию"); + return ModbusConfig(); + } + + if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + qInfo("Файл конфигурации защищен от чтения. Будут загружены параметры по умолчанию"); + return ModbusConfig(); + } + + ModbusConfig config; + while(!file.atEnd()) + { + QString line = file.readLine(); + line.chop(1); + QStringList partLine = line.split('='); + if(partLine.count() != 2) + continue; + QString& key = partLine[0]; + QString& value = partLine[1]; + if(m_HOST == key) + { + config.host = value; + continue; + } + if(m_PORT == key) + { + config.port = value.toUInt(); + continue; + } + if(m_POLL_FREQ == key) + { + config.pollFrequencyMs = value.toInt(); + continue; + } + if(m_TIMEOUT == key) + { + config.timeoutMs = value.toInt(); + continue; + } + if(m_RETRIES == key) + { + config.retries = value.toInt(); + continue; + } + if(m_UNIT_ID == key) + config.unitId = value.toInt(); + } + + file.close(); + return config; +} diff --git a/src/backend/data_processing/DataProcessor.cpp b/src/backend/data_processing/DataProcessor.cpp new file mode 100644 index 0000000..94e62af --- /dev/null +++ b/src/backend/data_processing/DataProcessor.cpp @@ -0,0 +1,263 @@ +#include "backend/data_processing/DataProcessor.h" + +// Этапы — нумерация условная, должна совпадать со StateMachine. +namespace Stage +{ + constexpr int IDLE = -1; + constexpr int COLD_CRANKING = 0; + constexpr int START_AND_WARMUP = 1; + constexpr int HOT_NO_LOAD = 2; + constexpr int HOT_WITH_LOAD = 3; +} + +// Порог предупреждения = 90% от критического. +static constexpr double WARN_RATIO = 0.9; + +DataProcessor::DataProcessor(const ModelConfig &config, QObject *parent) + : QObject(parent), m_config(config) +{ +} + +void DataProcessor::onStageChanged(int newStage) +{ + m_currentStage = newStage; +} + +void DataProcessor::onConfigChanged(ModelConfig config) +{ + m_config = config; +} + +void DataProcessor::reset() +{ + m_alarmLatched = false; + m_currentStage = Stage::IDLE; +} + +Decision DataProcessor::processFrame(const SensorFrame &frame) +{ + Decision decision; + + // После аварии — пустое решение (только аварийная остановка). + if (m_alarmLatched) + { + decision.state = DiagState::Alarm_Generic; + decision.reason = QStringLiteral("alarm latched"); + decision.controls = makeStopControls(); + emit decisionReady(decision); + return decision; + } + + const int stage = frame.stage; + + // Неактивные режимы — проверки выключены. + if (stage < Stage::COLD_CRANKING) + { + decision.state = DiagState::Ok; + decision.reason = QStringLiteral("inactive stage"); + emit decisionReady(decision); + return decision; + } + + const double maxRpm = + (stage == Stage::COLD_CRANKING) + ? static_cast(m_config.maxRpmPrir) + : static_cast(m_config.maxRpmRun); + + DiagState worstState = DiagState::Ok; + QString worstReason; + int worstSeverity = 0; // 0=ok, 1=prewarn, 2=alarm + + auto promote = [&](int severity, DiagState st, const QString &rsn) + { + if (severity > worstSeverity) + { + worstSeverity = severity; + worstState = st; + worstReason = rsn; + } + }; + + // обороты ДВС + if (isParamActiveOnStage("rpm", stage)) + { + if (frame.rpm >= maxRpm) + { + promote(2, DiagState::Alarm_RpmOverspeed, + QStringLiteral("Превышение оборотов: %1 >= %2").arg(frame.rpm).arg(maxRpm)); + } + else if (frame.rpm >= maxRpm * WARN_RATIO) + { + promote(1, DiagState::PreWarn_RpmHigh, + QStringLiteral("Обороты приближаются к пределу: %1").arg(frame.rpm)); + } + } + + // температура ДВС (охлаждающей жидкости) + if (isParamActiveOnStage("dieselTemp", stage)) + { + if (frame.dieselTemp >= m_config.maxDieselTemp) + { + promote(2, DiagState::Alarm_DieselOverheat, + QStringLiteral("Перегрев ДВС: %1").arg(frame.dieselTemp)); + } + else if (frame.dieselTemp >= m_config.maxDieselTemp * WARN_RATIO) + { + promote(1, DiagState::PreWarn_DieselTempHigh, + QStringLiteral("ДВС близок к перегреву: %1 (требуется охлаждение)") + .arg(frame.dieselTemp)); + } + } + + // температура АД + if (isParamActiveOnStage("motorTemp", stage)) + { + if (frame.motorTemp >= m_config.maxMotorTemp) + { + promote(2, DiagState::Alarm_MotorOverheat, + QStringLiteral("Перегрев АД: %1").arg(frame.motorTemp)); + } + else if (frame.motorTemp >= m_config.maxMotorTemp * WARN_RATIO) + { + promote(1, DiagState::PreWarn_MotorTempHigh, + QStringLiteral("АД близок к перегреву: %1 (требуется охлаждение)") + .arg(frame.motorTemp)); + } + } + + // температура балластных резисторов + if (isParamActiveOnStage("resistorTemp", stage)) + { + if (frame.resistorTemp >= m_config.maxResistorTemp) + { + promote(2, DiagState::Alarm_ResistorOverheat, + QStringLiteral("Перегрев балластных резисторов: %1").arg(frame.resistorTemp)); + } + else if (frame.resistorTemp >= m_config.maxResistorTemp * WARN_RATIO) + { + promote(1, DiagState::PreWarn_ResistorHigh, + QStringLiteral("Балластные резисторы близки к перегреву: %1 (требуется охлаждение)") + .arg(frame.resistorTemp)); + } + } + + // давление масла ДВС — верхняя граница + if (isParamActiveOnStage("dieselPressureMax", stage)) + { + if (frame.dieselPressure >= m_config.maxDieselPressure) + { + promote(2, DiagState::Alarm_PressureOver, + QStringLiteral("Превышение давления масла: %1").arg(frame.dieselPressure)); + } + else if (frame.dieselPressure >= m_config.maxDieselPressure * WARN_RATIO) + { + promote(1, DiagState::PreWarn_PressureHigh, + QStringLiteral("Давление масла близко к верхнему пределу: %1") + .arg(frame.dieselPressure)); + } + } + + // давление масла ДВС — нижняя граница + if (isParamActiveOnStage("dieselPressureMin", stage)) + { + const double warnLow = m_config.minDieselPressure + + m_config.minDieselPressure * (1.0 - WARN_RATIO); + if (frame.dieselPressure <= m_config.minDieselPressure) + { + promote(2, DiagState::Alarm_PressureUnder, + QStringLiteral("Падение давления масла: %1").arg(frame.dieselPressure)); + } + else if (frame.dieselPressure <= warnLow) + { + promote(1, DiagState::PreWarn_PressureLow, + QStringLiteral("Давление масла приближается к нижнему пределу: %1") + .arg(frame.dieselPressure)); + } + } + + // ---- Формирование Decision ---- + decision.state = worstState; + decision.reason = worstReason; + + if (worstSeverity == 2) + { + // Авария — фиксируем и формируем команду аварийной остановки. + m_alarmLatched = true; + decision.controls = makeStopControls(); + emit decisionReady(decision); + return decision; + } + + if (worstSeverity == 1) + { + // Предаварийное состояние — включаем соответствующие вентиляторы + // через CoilsRegisters (Fan_ICE / Fan_AD / Fan_Ballast). + ModelControl mc; + switch (worstState) + { + case DiagState::PreWarn_DieselTempHigh: + mc.type = ControlType::Fan_ICE; + mc.value = 1; + decision.controls.append(mc); + break; + case DiagState::PreWarn_MotorTempHigh: + mc.type = ControlType::Fan_AD; + mc.value = 1; + decision.controls.append(mc); + break; + case DiagState::PreWarn_ResistorHigh: + mc.type = ControlType::Fan_Ballast; + mc.value = 1; + decision.controls.append(mc); + break; + // Для предупреждений по оборотам и давлению масла + // DataProcessor только выставляет DiagState и reason — + // конкретные действия выполняет потребитель Decision. + case DiagState::PreWarn_RpmHigh: + case DiagState::PreWarn_PressureHigh: + case DiagState::PreWarn_PressureLow: + default: + break; + } + } + + emit decisionReady(decision); + return decision; +} + +bool DataProcessor::isParamActiveOnStage(const QString ¶m, int stage) const +{ + if (stage < Stage::COLD_CRANKING) + { + return false; + } + // давление и температура ДВС — после прогрева. + if (param == "dieselPressureMin" || + param == "dieselPressureMax" || + param == "dieselTemp") + { + return (stage == Stage::START_AND_WARMUP || + stage == Stage::HOT_NO_LOAD || + stage == Stage::HOT_WITH_LOAD); + } + // балластные резисторы — на этапе с нагрузкой. + if (param == "resistorTemp") + { + return (stage == Stage::HOT_WITH_LOAD); + } + // rpm и motorTemp — на всех активных этапах. + return true; +} + +QVector DataProcessor::makeStopControls() +{ + QVector v; + // Аварийная остановка симуляции через SimulationCommand. + v.append({ControlType::SimulationCommand, + static_cast(SimulationCommand::EmergencyStop)}); + // Принудительно включаем все вентиляторы охлаждения. + v.append({ControlType::Fan_ICE, 1}); + v.append({ControlType::Fan_AD, 1}); + v.append({ControlType::Fan_Ballast, 1}); + return v; +} diff --git a/src/backend/data_store/DataStore.cpp b/src/backend/data_store/DataStore.cpp new file mode 100644 index 0000000..59db6e7 --- /dev/null +++ b/src/backend/data_store/DataStore.cpp @@ -0,0 +1,577 @@ +#include +#include +#include +#include + +#include +#include + +#include "backend/data_store/DataStore.h" + +// Low-level layer +// === Connector === +Connector::Connector(std::string path, size_t record_size) : path_(std::move(path)), size_(0), record_size_(record_size) {} + +size_t Connector::getSize() const noexcept +{ + return size_; +} + +size_t Connector::getRecordSize() const noexcept +{ + return record_size_; +} + +const std::string &Connector::getPath() const noexcept +{ + return path_; +} + +void Connector::setSize(size_t size) noexcept +{ + size_ = size; +} + +// === FileConnector === +size_t FileConnector::getFullRecordSize() const noexcept +{ + return getRecordSize() + 1; +} + +bool FileConnector::isValidId(int64_t id) const noexcept +{ + return id >= 0 && static_cast(id) < getSize(); +} + +std::streampos FileConnector::getOffset(int64_t id) const noexcept +{ + return static_cast(id * getFullRecordSize()); +} + +std::string FileConnector::normalizeRecord(const std::string &data) const +{ + std::string fixed = data; + + if (fixed.size() < getRecordSize()) + { + std::cerr << "[FileConnector] " + << "Data is smaller than record size. " + << "Padding will be applied.\n"; + + fixed.append(getRecordSize() - fixed.size(), ' '); + } + else if (fixed.size() > getRecordSize()) + { + std::cerr << "[FileConnector] " + << "Data is larger than record size. " + << "Data will be truncated.\n"; + + fixed = fixed.substr(0, getRecordSize()); + } + + return fixed; +} + +bool FileConnector::seekRead(std::fstream &file, std::streampos offset) const +{ + file.seekg(offset); + + if (!file) + { + std::cerr << "[FileConnector] " + << "seekg failed. Offset: " + << offset << '\n'; + + return false; + } + + return true; +} + +bool FileConnector::seekWrite(std::fstream &file, std::streampos offset) const +{ + file.seekp(offset); + + if (!file) + { + std::cerr << "[FileConnector] " + << "seekp failed. Offset: " + << offset << '\n'; + + return false; + } + + return true; +} + +bool FileConnector::readRecord(std::fstream &file, std::streampos offset, std::string &buffer) const +{ + if (!seekRead(file, offset)) + { + return false; + } + + buffer.resize(getRecordSize()); + file.read(buffer.data(), static_cast(getRecordSize())); + + if (!file) + { + std::cerr << "[FileConnector] " + << "Failed to read record. Offset: " + << offset << '\n'; + + return false; + } + + return true; +} + +bool FileConnector::writeRecord(std::fstream &file, std::streampos offset, const std::string &data) +{ + if (!seekWrite(file, offset)) + { + return false; + } + + file.write(data.data(), static_cast(getRecordSize())); + file.put(RECORD_DELIMITER); + + if (!file) + { + std::cerr << "[FileConnector] " + << "Failed to write record. Offset: " + << offset << '\n'; + + return false; + } + + return true; +} + +// === Constructor === +FileConnector::FileConnector(std::string path, size_t record_size) : Connector(std::move(path), record_size) +{ + std::ifstream file(getPath(), std::ios::binary); + if (!file.is_open()) + { + std::cerr << "[FileConnector] " + << "Failed to open file: " + << getPath() << '\n'; + setSize(0); + return; + } + + file.seekg(0, std::ios::end); + size_t file_size = static_cast(file.tellg()); + + if (file_size % getFullRecordSize() != 0) + { + std::cerr << "[FileConnector] " + << "Invalid file size. " + << "File may be corrupted.\n"; + } + + setSize(file_size / getFullRecordSize()); +} + +int64_t FileConnector::write(const std::string &data) +{ + const std::string path = getPath(); + + // Создаём файл, если его нет + if (!std::filesystem::exists(path)) + { + std::ofstream create_file(path, std::ios::binary); + if (!create_file.is_open()) + { + std::cerr << "[FileConnector::write] Failed to create file.\n"; + return -1; + } + } + + std::fstream file(path, std::ios::in | std::ios::out | std::ios::binary); + + if (!file.is_open()) + { + std::cerr << "[FileConnector::write] Failed to open file.\n"; + return -1; + } + + std::string fixed = normalizeRecord(data); + int64_t id = static_cast(getSize()); + std::streampos offset = getOffset(id); + + if (!writeRecord(file, offset, fixed)) + { + return -1; + } + + setSize(getSize() + 1); + + return id; +} + +// === Read === +std::optional FileConnector::read(int64_t id) const +{ + if (!isValidId(id)) + { + std::cerr << "[FileConnector::read] " + << "Invalid id: " + << id << '\n'; + + return std::nullopt; + } + + std::fstream file(getPath(), std::ios::in | std::ios::binary); + + if (!file.is_open()) + { + std::cerr << "[FileConnector::read] " + << "Failed to open file.\n"; + + return std::nullopt; + } + + std::string buffer; + + if (!readRecord(file, getOffset(id), buffer)) + { + return std::nullopt; + } + + return buffer; +} + +// === Update === +bool FileConnector::update(int64_t id, const std::string &data) +{ + if (!isValidId(id)) + { + std::cerr << "[FileConnector::update] " + << "Invalid id: " + << id << '\n'; + + return false; + } + + std::fstream file(getPath(), std::ios::in | std::ios::out | std::ios::binary); + + if (!file.is_open()) + { + std::cerr << "[FileConnector::update] " + << "Failed to open file.\n"; + + return false; + } + + std::string fixed = normalizeRecord(data); + return writeRecord(file, getOffset(id), fixed); +} + +// === Remove === +bool FileConnector::remove(int64_t id) +{ + if (!isValidId(id)) + { + std::cerr << "[FileConnector::remove] Invalid id: " << id << '\n'; + return false; + } + + std::fstream file(getPath(), std::ios::in | std::ios::out | std::ios::binary); + + if (!file.is_open()) + { + std::cerr << "[FileConnector::remove] Failed to open file.\n"; + return false; + } + + size_t last_id = getSize() - 1; + for (size_t current_id = static_cast(id); current_id < last_id; ++current_id) + { + std::string buffer; + + // читаем следующую запись + if (!readRecord(file, getOffset(static_cast(current_id + 1)), buffer)) + { + return false; + } + + // пересчёт id внутри строки + size_t delimiter = buffer.find(';'); + + if (delimiter == std::string::npos) + { + std::cerr << "[FileConnector::remove] " + << "Malformed record: no id delimiter.\n"; + return false; + } + + buffer.replace(0, delimiter, std::to_string(current_id)); + + // записываем в текущую позицию + if (!writeRecord(file, getOffset(static_cast(current_id)), buffer)) + { + return false; + } + } + + file.close(); + + try + { + std::filesystem::resize_file(getPath(), last_id * getFullRecordSize()); + } + catch (const std::exception &e) + { + std::cerr << "[FileConnector::remove] resize_file failed: " + << e.what() << '\n'; + + return false; + } + + setSize(last_id); + + return true; +} + +void printRecord(FileConnector &connector, int64_t id) +{ + auto record = connector.read(id); +} + +// === DataStore (Qt layer) === +namespace +{ +QString diagStateToString(DiagState state) +{ + return QString::number(static_cast(state)); +} + +bool stringToDiagState(const QString &value, DiagState &state) +{ + bool ok = false; + const int raw = value.trimmed().toInt(&ok); + if (!ok) + { + return false; + } + + state = static_cast(raw); + return true; +} +} // namespace + +qint64 DataStore::generateId() noexcept +{ + if (!connector_) + { + return -1; + } + + return static_cast(connector_->getSize()); +} + +DataStore::DataStore(Connector *connector, QObject *parent) : QObject(parent), connector_(connector) +{ + if (!connector_) + { + qWarning().noquote() << "[DataStore] Null connector passed to constructor."; + return; + } +} + +QString DataStore::serializeData(qint64 id, + const Data &record, + int precision) const +{ + const auto &frame = record.frame; + + auto fmt = [precision](double value) + { + return QString::number(value, 'f', precision); + }; + + QStringList parts; + parts.reserve(10); + + parts << QString::number(id) + << fmt(frame.dieselTemp) + << fmt(frame.motorTemp) + << fmt(frame.resistorTemp) + << fmt(frame.dieselPressure) + << fmt(frame.torque) + << fmt(frame.rpm) + << QString::number(frame.timestampMs) + << QString::number(frame.stage) + << diagStateToString(record.state); + + return parts.join(';'); +} + +bool DataStore::deserializeData(const QString &line, Data &data) const +{ + const QStringList parts = line.trimmed().split(';', Qt::KeepEmptyParts); + + if (parts.size() != 10) + { + qWarning().noquote() + << "[DataStore] Invalid record format. Expected 10 fields, got" + << parts.size() + << "Line:" << line; + return false; + } + + bool ok = false; + + // parts[0] = id (игнорируем) + + data.frame.dieselTemp = parts.at(1).trimmed().toDouble(&ok); + if (!ok) return false; + + data.frame.motorTemp = parts.at(2).trimmed().toDouble(&ok); + if (!ok) return false; + + data.frame.resistorTemp = parts.at(3).trimmed().toDouble(&ok); + if (!ok) return false; + + data.frame.dieselPressure = parts.at(4).trimmed().toDouble(&ok); + if (!ok) return false; + + data.frame.torque = parts.at(5).trimmed().toDouble(&ok); + if (!ok) return false; + + data.frame.rpm = parts.at(6).trimmed().toDouble(&ok); + if (!ok) return false; + + data.frame.timestampMs = parts.at(7).trimmed().toLongLong(&ok); + if (!ok) return false; + + data.frame.stage = parts.at(8).trimmed().toInt(&ok); + if (!ok) return false; + + if (!stringToDiagState(parts.at(9), data.state)) + { + qWarning().noquote() + << "[DataStore] Failed to parse diagnostic state:" << parts.at(9); + return false; + } + + return true; +} + +bool DataStore::writeData(const Data &record) +{ + if (!connector_) + { + qWarning().noquote() << "[DataStore] writeData failed: connector is null."; + return false; + } + + const qint64 id = generateId(); + if (id < 0) + { + qWarning().noquote() << "[DataStore] writeData failed: unable to generate id."; + return false; + } + + const QString serialized = serializeData(id, record); + const QByteArray payload = serialized.toUtf8(); + + if (static_cast(payload.size()) > connector_->getRecordSize()) + { + qWarning().noquote() + << "[DataStore] Record is too large for FileConnector fixed record size." + << "Size:" << payload.size() + << "Limit:" << connector_->getRecordSize() + << "Data:" << serialized; + return false; + } + + if (connector_->write(payload.toStdString()) < 0) + { + qWarning().noquote() << "[DataStore] Failed to write record."; + return false; + } + + return true; +} + +QVector DataStore::readData(qint64 id) const +{ + QVector result; + + if (!connector_) + { + qWarning().noquote() << "[DataStore] readData failed: connector is null."; + return result; + } + + if (id < 0 || static_cast(id) >= connector_->getSize()) + { + qWarning().noquote() << "[DataStore] readData failed: invalid id:" << id; + return result; + } + + const std::optional raw = connector_->read(id); + if (!raw.has_value()) + { + qWarning().noquote() + << "[DataStore] readData failed: unable to read record with id" + << id; + return result; + } + + Data data{}; + if (!deserializeData(QString::fromStdString(*raw), data)) + { + qWarning().noquote() + << "[DataStore] readData failed: deserialize error for id" + << id; + return result; + } + + result.append(data); + return result; +} + +QVector DataStore::readAllData() const +{ + QVector result; + + if (!connector_) + { + qWarning().noquote() << "[DataStore] readAllData failed: connector is null."; + return result; + } + + const size_t size = connector_->getSize(); + result.reserve(static_cast(size)); + + for (size_t i = 0; i < size; ++i) + { + const std::optional raw = + connector_->read(static_cast(i)); + + if (!raw.has_value()) + { + qWarning().noquote() + << "[DataStore] Skipping unreadable record with id" + << static_cast(i); + continue; + } + + Data data{}; + if (!deserializeData(QString::fromStdString(*raw), data)) + { + qWarning().noquote() + << "[DataStore] Skipping malformed record with id" + << static_cast(i); + continue; + } + + result.append(data); + } + + return result; +} \ No newline at end of file diff --git a/src/backend/modbus_client/IModbusBridge.cpp b/src/backend/modbus_client/IModbusBridge.cpp new file mode 100644 index 0000000..afe3794 --- /dev/null +++ b/src/backend/modbus_client/IModbusBridge.cpp @@ -0,0 +1,3 @@ +#include "backend/modbus_client/IModbusBridge.h" + +IModbusBridge::IModbusBridge(const ModbusConfig& cfg, QObject* parent) : QObject(parent) {} diff --git a/src/backend/modbus_client/MockModbusBridge.cpp b/src/backend/modbus_client/MockModbusBridge.cpp new file mode 100644 index 0000000..4ff1201 --- /dev/null +++ b/src/backend/modbus_client/MockModbusBridge.cpp @@ -0,0 +1,48 @@ +#include "backend/DataTypes.h" +#include "backend/modbus_client/MockModbusBridge.h" +#include +#include +#include + +MockModbusBridge::MockModbusBridge(const ModbusConfig& cfg, QObject* parent) : IModbusBridge(cfg, parent) +{ + m_cfg = cfg; + QObject::connect(&m_pollTimer, &QTimer::timeout, this, &MockModbusBridge::onReadSensors); + qInfo() << "[MockModbusBridge] Initialization is successfull"; + QThread::msleep(1000); + qInfo() << "[MockModbusBridge] Connected to the Modbus Server"; +} + +void MockModbusBridge::startPolling() +{ + qInfo() << "[MockModbusBridge] Starting to poll server"; + m_pollTimer.start(m_cfg.pollFrequencyMs); +} + +void MockModbusBridge::stopPolling() +{ + qInfo() << "[MockModbusBridge] Stopping to poll server"; + m_pollTimer.stop(); +} + +void MockModbusBridge::onReadSensors() +{ + qInfo() << "[MockModbusBridge] Requesting data"; + SensorFrame frame = { 1, 1, 1, 1, 1, 1, 1 }; + emit sensorsDataReady(frame); +} + +void MockModbusBridge::onReadInfo() +{ + qInfo() << "[MockModbusBridge] Requesting model info"; +} + +void MockModbusBridge::onWriteConfig(const ModelConfig& cmd) +{ + qInfo() << "[MockModbusBridge] Writing a new configuration to the model"; +} + +void MockModbusBridge::onWriteDecision(const Decision& decision) +{ + qInfo() << "[MockModbusBridge] Sending a decision to the model"; +} diff --git a/src/backend/modbus_client/QtModbusBridge.cpp b/src/backend/modbus_client/QtModbusBridge.cpp new file mode 100644 index 0000000..d19ee43 --- /dev/null +++ b/src/backend/modbus_client/QtModbusBridge.cpp @@ -0,0 +1,363 @@ +#include "backend/DataTypes.h" +#include "backend/modbus_client/QtModbusBridge.h" +#include +#include +#include +#include +#include + +QtModbusBridge::QtModbusBridge(const ModbusConfig& cfg, QObject* parent) : IModbusBridge(cfg, parent) +{ + m_cfg = cfg; + + m_client.setConnectionParameter(QModbusDevice::NetworkAddressParameter, QVariant::fromValue(m_cfg.host)); + m_client.setConnectionParameter(QModbusDevice::NetworkPortParameter, m_cfg.port); + m_client.setTimeout(m_cfg.timeoutMs); + m_client.setNumberOfRetries(m_cfg.retries); + + QObject::connect(&m_pollTimer, &QTimer::timeout, this, &QtModbusBridge::onReadSensors); + QObject::connect(&m_client, &QModbusDevice::stateChanged, this, &QtModbusBridge::onStateChange); + QObject::connect(&m_client, &QModbusDevice::errorOccurred, this, &QtModbusBridge::onErrorOccured); + + qDebug() << "[QtModbusBridge] Initialization is successful"; + + m_client.connectDevice(); +} + +QtModbusBridge::~QtModbusBridge() +{ + m_client.disconnectDevice(); +} + +void QtModbusBridge::onStateChange(QModbusDevice::State state) +{ + // Handling states change + if (state == QModbusDevice::UnconnectedState) + { + qDebug() << "[QtModbusBridge] Modbus client and server are disconnected"; + } + else if (state == QModbusDevice::ConnectingState) + { + qDebug() << "[QtModbusBridge] Connecting to the modbus device..."; + } + else if (state == QModbusDevice::ConnectedState) + { + qDebug() << "[QtModbusBridge] Connected to the modbus server successfully"; + } + else if (state == QModbusDevice::ClosingState) + { + qDebug() << "[QtModbusBridge] Closing the modbus device..."; + } + // Handling state transitions + if (state == QModbusDevice::ConnectedState && m_prevState != QModbusDevice::ConnectedState) + { + emit connectionRestored(); + } + else if (m_prevState == QModbusDevice::ConnectedState && state != QModbusDevice::ConnectedState) + { + emit connectionLost(); + } + m_prevState = state; +} + +void QtModbusBridge::onErrorOccured() +{ + QModbusDevice::Error error = m_client.error(); + if (error == QModbusDevice::NoError) + { + return; + } + + QString reason = m_client.errorString(); + if (error == QModbusDevice::ConfigurationError) + { + emit configurationError(reason); + } + else if (error == QModbusDevice::ConnectionError) + { + emit connectionError(reason); + } + else if (error != QModbusDevice::UnknownError) + { + emit requestError(reason); + } + else + { + emit generalError(reason); + } +} + +void QtModbusBridge::startPolling() +{ + qDebug() << "[QtModbusBridge] Starting to poll server"; + m_pollTimer.start(m_cfg.pollFrequencyMs); +} + +void QtModbusBridge::stopPolling() +{ + qDebug() << "[QtModbusBridge] Stopping to poll server"; + m_pollTimer.stop(); +} + +void QtModbusBridge::onReadSensors() +{ + qDebug() << "[QtModbusBridge] Requesting sensors data"; + + QModbusDataUnit dataUnit(QModbusDataUnit::InputRegisters, 0, InputRegisters::count); + + // Sending request to the device with specified Unit Id + auto* reply = m_client.sendReadRequest(dataUnit, m_cfg.unitId); + if (!reply) + { + return; + } + + // Handling reply with finished signal + QObject::connect(reply, &QModbusReply::finished, this, [this, reply]() + { + qDebug() << "[QtModbusBridge] Received reply with sensor data, begin to extract values"; + auto values = extractValues(reply, InputRegisters::count); + if (!values.has_value()) + { + qWarning() << "[QtModbusBridge] Failed to extract sensor data"; + return; + } + parseSensorsResponse(values.value()); + reply->deleteLater(); + }); +} + +void QtModbusBridge::onReadInfo() +{ + qDebug() << "[QtModbusBridge] Requesting model info"; + + QModbusDataUnit dataUnit(QModbusDataUnit::HoldingRegisters, 0, HoldingRegisters::count); + + // Sending request to the device with specified Unit Id + auto* reply = m_client.sendReadRequest(dataUnit, m_cfg.unitId); + if (!reply) + { + return; + } + + // Handling reply with finished signal + QObject::connect(reply, &QModbusReply::finished, this, [this, reply]() + { + qDebug() << "[QtModbusBridge] Received reply with model info data, begin to extract values"; + auto values = extractValues(reply, HoldingRegisters::count); + if (!values.has_value()) + { + qWarning() << "[QtModbusBridge] Failed to extract model info data"; + return; + } + parseInfoResponse(values.value()); + reply->deleteLater(); + }); +} + +void QtModbusBridge::onWriteConfig(const ModelConfig& cmd) +{ + qDebug() << "[QtModbusBridge] Writing a new configuration to the model"; + + QVector regs; + regs.reserve(9); + + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::omega_ICE_max_prir, + m_reg_converter.toRegisterWords(cmd.maxRpmRun) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::omega_ICE_max_run, + m_reg_converter.toRegisterWords(cmd.maxDieselPressure) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::P_oil_max, + m_reg_converter.toRegisterWords(cmd.maxDieselPressure) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::P_oil_min, + m_reg_converter.toRegisterWords(cmd.minDieselPressure) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::T_cool_max, + m_reg_converter.toRegisterWords(cmd.maxDieselTemp) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::T_AD_max, + m_reg_converter.toRegisterWords(cmd.maxMotorTemp) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::T_ballast_max, + m_reg_converter.toRegisterWords(cmd.maxResistorTemp) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::f_AD_Input, + m_reg_converter.toRegisterWords(cmd.freqAD) + ); + regs.emplace_back( + QModbusDataUnit::HoldingRegisters, + HoldingRegisters::M_AD_target, + m_reg_converter.toRegisterWords(cmd.momentAD) + ); + writeRegisterVector(regs); +} + +void QtModbusBridge::onWriteDecision(const Decision& decision) +{ + qDebug() << "[QtModbusBridge] Sending a decision to the model"; + QVector regs; + regs.reserve(decision.controls.size()); + for (const auto& control : decision.controls) + { + QModbusDataUnit::RegisterType regType = QModbusDataUnit::Invalid; + qsizetype startAddress = -1; + switch(control.type) + { + case ControlType::SimulationCommand: + regType = QModbusDataUnit::HoldingRegisters; + startAddress = HoldingRegisters::simulationCommand; + break; + case ControlType::SimulationMode: + regType = QModbusDataUnit::HoldingRegisters; + startAddress = HoldingRegisters::simulationMode; + break; + case ControlType::SimulationRequest: + regType = QModbusDataUnit::HoldingRegisters; + startAddress = HoldingRegisters::simulationRequest; + break; + case ControlType::Fan_AD: + regType = QModbusDataUnit::Coils; + startAddress = CoilsRegisters::fan_AD; + break; + case ControlType::Fan_Ballast: + regType = QModbusDataUnit::Coils; + startAddress = CoilsRegisters::fan_ballast; + break; + case ControlType::Fan_ICE: + regType = QModbusDataUnit::Coils; + startAddress = CoilsRegisters::fan_ICE; + break; + case ControlType::None: + break; + default: + qWarning() << "[QtModbusBridge] Unexpected ControlType was received in onWriteDecision"; + } + if (startAddress == -1) + continue; + regs.emplace_back( + regType, + startAddress, + QVector{ static_cast(control.value) } + ); + } + writeRegisterVector(regs); +} + +std::optional> QtModbusBridge::extractValues(QModbusReply* reply, qsizetype expectedSize) +{ + if (reply->error() != QModbusDevice::NoError) + { + return std::nullopt; + } + + const QModbusDataUnit result = reply->result(); + const QVector values = result.values(); + + if (values.size() != expectedSize) + { + qWarning() << "[extractValues] size mismatch, values.size() =" << values.size() + << ", expectedSize =" << expectedSize; + emit requestError("Size of received data differs from expected size"); + return std::nullopt; + } + + return values; +} + +void QtModbusBridge::parseSensorsResponse(const QVector& values) +{ + qDebug() << "[parseSensorsResponse] entered, values.size() =" << values.size(); + + SensorFrame frame = { + m_reg_converter.fromRegisterWordDouble(&values[InputRegisters::T_cool]), + m_reg_converter.fromRegisterWordDouble(&values[InputRegisters::T_AD]), + m_reg_converter.fromRegisterWordDouble(&values[InputRegisters::T_ballast]), + m_reg_converter.fromRegisterWordDouble(&values[InputRegisters::P_oil]), + m_reg_converter.fromRegisterWordDouble(&values[InputRegisters::M_AD]), + m_reg_converter.fromRegisterWordDouble(&values[InputRegisters::f_AD]), + m_reg_converter.fromRegisterWordQint(&values[InputRegisters::timestamp_ir]), + 1 // TODO: clarify what does stage mean + }; + emit sensorsDataReady(frame); +} + +void QtModbusBridge::parseInfoResponse(const QVector& values) +{ + ModelConfig info = { + m_reg_converter.fromRegisterWordDouble(&values[HoldingRegisters::T_cool_max]), + m_reg_converter.fromRegisterWordDouble(&values[HoldingRegisters::T_AD_max]), + m_reg_converter.fromRegisterWordDouble(&values[HoldingRegisters::T_ballast_max]), + m_reg_converter.fromRegisterWordDouble(&values[HoldingRegisters::P_oil_max]), + m_reg_converter.fromRegisterWordDouble(&values[HoldingRegisters::P_oil_min]), + m_reg_converter.fromRegisterWordInt(&values[HoldingRegisters::omega_ICE_max_prir]), + m_reg_converter.fromRegisterWordInt(&values[HoldingRegisters::omega_ICE_max_run]), + }; + emit modelInfoReady(info); +} + +void QtModbusBridge::writeRegisterVector(QVector regs) +{ + if (regs.empty()) + return; + + std::sort(regs.begin(), regs.end(), [](const auto& lhs, const auto& rhs) { + return std::tie(lhs.type, lhs.startAddress) < std::tie(rhs.type, rhs.startAddress); + }); + + QModbusDataUnit::RegisterType batchType = regs[0].type; + quint16 batchStart = regs[0].startAddress; + quint16 expectedAddress = batchStart; + + QVector batchValues; + batchValues.reserve(regs.size()); + + auto flush = [&]() + { + if (batchValues.isEmpty()) + return; + + QModbusDataUnit unit( + batchType, + batchStart, + batchValues + ); + + m_client.sendWriteRequest(unit, m_cfg.unitId); + }; + + for (const RegisterBlock& reg : regs) + { + if (reg.startAddress != expectedAddress || reg.type != batchType) + { + flush(); + + batchType = reg.type; + batchStart = reg.startAddress; + batchValues.clear(); + expectedAddress = reg.startAddress; + } + + batchValues.append(reg.values); + expectedAddress += reg.values.size(); + } + + flush(); +} \ No newline at end of file diff --git a/src/backend/modbus_client/RegisterConverter.cpp b/src/backend/modbus_client/RegisterConverter.cpp new file mode 100644 index 0000000..1a5619b --- /dev/null +++ b/src/backend/modbus_client/RegisterConverter.cpp @@ -0,0 +1,32 @@ +#include "backend/modbus_client/RegisterConverter.h" +#include + +double RegisterConverter::fromRegisterWordDouble(const quint16* value) +{ + return static_cast(fromRegisterWordRawValue(value, 4)); +} + +qint64 RegisterConverter::fromRegisterWordQint(const quint16* value) +{ + return static_cast(fromRegisterWordRawValue(value, 4)); +} + +int RegisterConverter::fromRegisterWordInt(const quint16* value) +{ + return static_cast(fromRegisterWordRawValue(value, 4)); +} + +quint64 RegisterConverter::fromRegisterWordRawValue(const quint16* value, qsizetype count) +{ + if (count > 4) + { + qWarning() << "Can't read into quint64 more than 4 registers, function returned 0"; + return 0; + } + quint64 res = 0; + for (qsizetype i = 0; i < count; ++i) + { + res |= static_cast(*(value + i)) << (count - i - 1) * 16; + } + return res; +} \ No newline at end of file diff --git a/src/backend/state_machine/state_machine.cpp b/src/backend/state_machine/state_machine.cpp new file mode 100644 index 0000000..7dd555e --- /dev/null +++ b/src/backend/state_machine/state_machine.cpp @@ -0,0 +1,225 @@ +#include "backend/state_machine/state_machine.h" +#include "backend/backend_worker/backendworker.h" +#include "backend/modbus_client/MockModbusBridge.h" +#include "backend/data_store/DataStore.h" +#include + +StateMachine::StateMachine(BackendWorker* backendWorker, QObject* parent) + : QObject(parent) + , m_communicator(new BackendCommunicator(this)) + , m_pollTimer(new QTimer(this)) + , m_stageTimer(new QTimer(this)) +{ + ModbusConfig modbusCfg = ConfigData().LoadConfig(); + m_modbusBridge = new MockModbusBridge(modbusCfg, this); + + // ----- Подготавливаем CSV-коннекторы для хранилища ----- + // пути можно вынести в настройки, здесь для примера + size_t SIZE_FILE_STR = 10; + m_Connector = new FileConnector("measurements.csv", SIZE_FILE_STR); + + // ----- Создаём DataStore ----- + m_dataStore = new DataStore(m_Connector, this); + + m_dataProcessor = new DataProcessor(ModelConfig{}, this); + + //Коннекты для коммуникатора + connect(backendWorker, &BackendWorker::SendedFrontControlToBackend, + m_communicator, &BackendCommunicator::ReceivedFrontControl, Qt::QueuedConnection); + connect(backendWorker, &BackendWorker::StartedEngine, + m_communicator, &BackendCommunicator::ReceivedStartEngine, Qt::QueuedConnection); + connect(backendWorker, &BackendWorker::StopedEngine, + m_communicator, &BackendCommunicator::ReceivedStopEngine, Qt::QueuedConnection); + + connect(m_communicator, &BackendCommunicator::SendedSensorFrame, + backendWorker, &BackendWorker::ReceivedSensorFrame, Qt::QueuedConnection); + connect(m_communicator, &BackendCommunicator::SendedEmergencyStopInfo, + backendWorker, &BackendWorker::ReceivedEmergencyStopInfo, Qt::QueuedConnection); + connect(m_communicator, &BackendCommunicator::SendedFeedback, + backendWorker, &BackendWorker::ReceivedFeedback, Qt::QueuedConnection); + connect(m_communicator, &BackendCommunicator::SendedData, + backendWorker, &BackendWorker::ReceivedData, Qt::QueuedConnection); + connect(m_communicator, &BackendCommunicator::SendedWarnToFrontend, + backendWorker, &BackendWorker::ReceivedWarn, Qt::QueuedConnection); + connect(m_communicator, &BackendCommunicator::SendedStageCompleteInfoToFrontend, + backendWorker, &BackendWorker::ReceivedStageCompleteInfo, Qt::QueuedConnection); + + connect(m_communicator, &BackendCommunicator::ReceivedFrontControl, + this, &StateMachine::onReceivedFrontControl); + + connect(m_modbusBridge, &IModbusBridge::sensorsDataReady, + this, &StateMachine::onSensorsDataReady); + connect(m_modbusBridge, &IModbusBridge::configurationError, + this, &StateMachine::onModbusConfigurationError); + connect(m_modbusBridge, &IModbusBridge::connectionError, + this, &StateMachine::onModbusConnectionError); + connect(m_modbusBridge, &IModbusBridge::requestError, + this, &StateMachine::onModbusRequestError); + connect(m_modbusBridge, &IModbusBridge::connectionLost, + this, &StateMachine::onModbusConnectionLost); + connect(m_modbusBridge, &IModbusBridge::connectionRestored, + this, &StateMachine::onModbusConnectionRestored); + + // connect(m_dataProcessor, &DataProcessor::decisionReady, + // this, &StateMachine::onDecisionReady); + + connect(m_pollTimer, &QTimer::timeout, this, &StateMachine::pollModbus); +} + +StateMachine::~StateMachine() { stop(); } + +void StateMachine::start() +{ + // m_config = config; + m_dataProcessor->onConfigChanged(m_config); + m_dataProcessor->reset(); + m_currentRunId = static_cast(QDateTime::currentSecsSinceEpoch()); + + int pollMs = ConfigData().LoadConfig().pollFrequencyMs; + m_pollTimer->start(pollMs); + + Data startEvt; + startEvt.frame.timestampMs = QDateTime::currentMSecsSinceEpoch(); + startEvt.frame.stage = -1; + startEvt.state = DiagState::Ok; + m_dataStore->writeData(startEvt); + + transitionTo(DiagState::Ok); + m_currentStageIndex = 0; + m_modbusBridge->onReadSensors(); +} + +void StateMachine::stop() +{ + m_pollTimer->stop(); + m_stageTimer->stop(); + m_modbusBridge->stopPolling(); + + Data stopEvt; + stopEvt.frame.timestampMs = QDateTime::currentMSecsSinceEpoch(); + stopEvt.frame.stage = m_currentStageIndex; + m_dataStore->writeData(stopEvt); + + transitionTo(DiagState::IDLE); + emit finished(m_state); +} + +void StateMachine::requestNextStage() { /* ... как раньше ... */ } +void StateMachine::requestAbort(const QString& reason) { /* ... */ } + +// ---------- Слоты ---------- +void StateMachine::pollModbus() { + m_modbusBridge->onReadSensors(); +} + +void StateMachine::onSensorsDataReady(const SensorFrame& frame) +{ + m_currentSensorFrame = frame; + m_currentSensorFrame.timestampMs = QDateTime::currentMSecsSinceEpoch(); + m_currentSensorFrame.stage = m_currentStageIndex; + + m_dataProcessor->processFrame(m_currentSensorFrame); + + // Отправка сырого кадра фронту через метод коммуникатора + m_communicator->SendSensorFrameToFrontend(m_currentSensorFrame); +} + +void StateMachine::onDecisionReady(const Decision& decision) +{ + // Запись в хранилище + Data rec; + rec.frame.stage = m_currentSensorFrame.stage; + rec.frame.timestampMs = m_currentSensorFrame.timestampMs; + rec.frame.rpm = m_currentSensorFrame.rpm; + rec.frame.torque = m_currentSensorFrame.torque; + rec.frame.dieselTemp = m_currentSensorFrame.dieselTemp; + rec.frame.motorTemp = m_currentSensorFrame.motorTemp; + rec.frame.resistorTemp = 0; // маппинг уточнить + rec.frame.dieselPressure = m_currentSensorFrame.dieselPressure; + m_dataStore->writeData(rec); + + // Обработка смены состояния + if (decision.state != m_state) { + Data evt; + evt.frame.timestampMs = QDateTime::currentMSecsSinceEpoch(); + evt.frame.stage = m_currentStageIndex; + evt.state = DiagState::Ok; + m_dataStore->writeData(evt); + + applyControls(decision.controls); + transitionTo(decision.state); + + // Если аварийное состояние – уведомить фронт + if (decision.state >= DiagState::Alarm_RpmOverspeed) { + m_communicator->SendEmergencyStopInfoToFrontend(); + } + } + + // Отправка данных для фронта (вектор из одного элемента) + QVector dataVec; + Data data; + data.frame = m_currentSensorFrame; + data.state = decision.state; + dataVec.append(data); + m_communicator->SendDataToFrontend(dataVec); + + // Обратная связь: считается успешным, если состояние не аварийное + //bool ok = (decision.state == DiagState::Ok || decision.state == DiagState::PreWarn_RpmHigh /* и т.п.*/); + m_communicator->SendFeedbackToFrontend(decision.state); +} + +void StateMachine::onReceivedFrontControl() +{ + ModelControl control; + if (control.type == ControlType::SimulationRequest) { + requestAbort("Front control emergency stop"); + } + // другая логика +} + +void StateMachine::onReceivedModelConfig(const ModelConfig& config) +{ + m_config = config; + m_modbusBridge->onWriteConfig(config); + m_dataProcessor->onConfigChanged(config); +} + +void StateMachine::transitionTo(DiagState newState) { + if (m_state == newState) return; + m_state = newState; +} + +void StateMachine::applyControls(const QVector& controls) { + // Заглушка – реализовать после расширения IModbusBridge методами записи + for (const auto& ctrl : controls) Q_UNUSED(ctrl); +} + +// ---------- Ошибки Modbus ---------- +void StateMachine::onModbusConfigurationError(const QString& reason) { + // Логируем причину в хранилище + Data evt; + evt.frame.timestampMs = QDateTime::currentMSecsSinceEpoch(); + evt.frame.stage = m_currentStageIndex; + evt.state = DiagState::Alarm_Generic; + m_dataStore->writeData(evt); + m_communicator->SendEmergencyStopInfoToFrontend(); +} + +void StateMachine::onModbusConnectionError(const QString& reason) { + // аналогично + m_communicator->SendEmergencyStopInfoToFrontend(); +} + +void StateMachine::onModbusRequestError(const QString& reason) { + m_communicator->SendEmergencyStopInfoToFrontend(); +} + +void StateMachine::onModbusConnectionLost() { + m_communicator->SendEmergencyStopInfoToFrontend(); + m_pollTimer->stop(); +} + +void StateMachine::onModbusConnectionRestored() { + if (m_state != DiagState::IDLE && m_state != DiagState::Alarm_Generic) + m_pollTimer->start(); +} diff --git a/src/backend/unit_tests/backend_worker_test/backend_worker_test.cpp b/src/backend/unit_tests/backend_worker_test/backend_worker_test.cpp new file mode 100644 index 0000000..f819885 --- /dev/null +++ b/src/backend/unit_tests/backend_worker_test/backend_worker_test.cpp @@ -0,0 +1,263 @@ +#include +#include +#include +#include "../../../include/backend/backend_worker/backendworker.h" +#include "../../../include/backend/DataTypes.h" + +class BackendWorkerTest : public QObject { + Q_OBJECT +private slots: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + + // Тесты конструктора и деструктора + void testConstructor(); + void testDestructor(); + + // Тесты запуска/остановки + void testRun(); + void testStop(); + void testRunStopSequence(); + + // Тесты отправки команд + void testSendFrontControlToBackend(); + void testSendModelConfigToBackend(); + + // Тесты сигналов получения данных + void testReceivedSensorFrame(); + void testReceivedEmergencyStopInfo(); + void testReceivedFeedback(); + void testReceivedData(); + + // Тесты внутренних сигналов + void testSendedFrontControlToBackend(); + void testSendedModelConfigToBackend(); + + // Тесты многопоточности + void testThreadSafety(); + void testMultipleRunStopCycles(); + +private: + BackendWorker* m_worker; + QThread* m_testThread; +}; + +void BackendWorkerTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); +} + +void BackendWorkerTest::cleanupTestCase() +{ +} + +void BackendWorkerTest::init() +{ + m_worker = new BackendWorker(); + m_testThread = new QThread(); + m_worker->moveToThread(m_testThread); + m_testThread->start(); +} + +void BackendWorkerTest::cleanup() +{ + m_testThread->quit(); + m_testThread->wait(); + delete m_worker; + delete m_testThread; +} + +void BackendWorkerTest::testConstructor() +{ + BackendWorker* worker = new BackendWorker(); + QVERIFY(worker != nullptr); + delete worker; +} + +void BackendWorkerTest::testDestructor() +{ + BackendWorker* worker = new BackendWorker(); + delete worker; + // Если деструктор отработал без ошибок - тест пройден + QVERIFY(true); +} + +void BackendWorkerTest::testRun() +{ + QSignalSpy runSpy(m_worker, &BackendWorker::Run); + + QMetaObject::invokeMethod(m_worker, "Run", Qt::QueuedConnection); + QTest::qWait(100); + + // Проверяем, что поток запустился + QVERIFY(m_testThread->isRunning()); +} + +void BackendWorkerTest::testStop() +{ + QMetaObject::invokeMethod(m_worker, "Run", Qt::QueuedConnection); + QTest::qWait(100); + + QMetaObject::invokeMethod(m_worker, "Stop", Qt::QueuedConnection); + QTest::qWait(100); + + // Проверяем, что поток остановился + QVERIFY(!m_testThread->isRunning()); +} + +void BackendWorkerTest::testRunStopSequence() +{ + for(int i = 0; i < 5; ++i) { + QMetaObject::invokeMethod(m_worker, "Run", Qt::QueuedConnection); + QTest::qWait(50); + QMetaObject::invokeMethod(m_worker, "Stop", Qt::QueuedConnection); + QTest::qWait(50); + } + QVERIFY(true); +} + +void BackendWorkerTest::testSendFrontControlToBackend() +{ + QSignalSpy internalSpy(m_worker, &BackendWorker::SendedFrontControlToBackend); + + FrontControl control; + // control.control = ControlType::NextStage; // Настройка в зависимости от реализации + + QMetaObject::invokeMethod(m_worker, "SendFrontControlToBackend", + Qt::QueuedConnection, Q_ARG(FrontControl&, control)); + QTest::qWait(100); + + QCOMPARE(internalSpy.count(), 1); +} + +void BackendWorkerTest::testSendModelConfigToBackend() +{ + QSignalSpy internalSpy(m_worker, &BackendWorker::SendedModelConfigToBackend); + + ModelConfig config; + config.maxRpm = 3000; + config.maxDieselTemp = 120; + config.maxMotorTemp = 100; + + QMetaObject::invokeMethod(m_worker, "SendModelConfigToBackend", + Qt::QueuedConnection, Q_ARG(ModelConfig&, config)); + QTest::qWait(100); + + QCOMPARE(internalSpy.count(), 1); +} + +void BackendWorkerTest::testReceivedSensorFrame() +{ + QSignalSpy spy(m_worker, &BackendWorker::ReceivedSensorFrame); + + SensorFrame frame; + frame.rpm = 1500; + frame.dieselTemp = 85; + frame.timestampMs = QDateTime::currentMSecsSinceEpoch(); + + emit m_worker->ReceivedSensorFrame(frame); + QTest::qWait(100); + + QCOMPARE(spy.count(), 1); + + QList arguments = spy.takeFirst(); + SensorFrame receivedFrame = arguments.at(0).value(); + QCOMPARE(receivedFrame.rpm, 1500); + QCOMPARE(receivedFrame.dieselTemp, 85); +} + +void BackendWorkerTest::testReceivedEmergencyStopInfo() +{ + QSignalSpy spy(m_worker, &BackendWorker::ReceivedEmergencyStopInfo); + + emit m_worker->ReceivedEmergencyStopInfo(); + QTest::qWait(100); + + QCOMPARE(spy.count(), 1); +} + +void BackendWorkerTest::testReceivedFeedback() +{ + QSignalSpy spy(m_worker, &BackendWorker::ReceivedFeedback); + + emit m_worker->ReceivedFeedback(true); + QTest::qWait(100); + + QCOMPARE(spy.count(), 1); + + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.at(0).toBool(), true); +} + +void BackendWorkerTest::testReceivedData() +{ + QSignalSpy spy(m_worker, &BackendWorker::ReceivedData); + + QVector dataVector; + // Добавление тестовых данных + + emit m_worker->ReceivedData(dataVector); + QTest::qWait(100); + + QCOMPARE(spy.count(), 1); +} + +void BackendWorkerTest::testSendedFrontControlToBackend() +{ + QSignalSpy spy(m_worker, &BackendWorker::SendedFrontControlToBackend); + + FrontControl control; + emit m_worker->SendedFrontControlToBackend(control); + QTest::qWait(100); + + QCOMPARE(spy.count(), 1); +} + +void BackendWorkerTest::testSendedModelConfigToBackend() +{ + QSignalSpy spy(m_worker, &BackendWorker::SendedModelConfigToBackend); + + ModelConfig config; + emit m_worker->SendedModelConfigToBackend(config); + QTest::qWait(100); + + QCOMPARE(spy.count(), 1); +} + +void BackendWorkerTest::testThreadSafety() +{ + QAtomicInt counter = 0; + QSignalSpy spy(m_worker, &BackendWorker::ReceivedSensorFrame); + + // Эмитируем сигналы из разных потоков + for(int i = 0; i < 10; ++i) { + SensorFrame frame; + frame.rpm = i * 100; + QMetaObject::invokeMethod(m_worker, [&]() { + emit m_worker->ReceivedSensorFrame(frame); + counter.ref(); + }, Qt::QueuedConnection); + } + + QTest::qWait(500); + QCOMPARE(counter.load(), 10); + QCOMPARE(spy.count(), 10); +} + +void BackendWorkerTest::testMultipleRunStopCycles() +{ + for(int i = 0; i < 3; ++i) { + QMetaObject::invokeMethod(m_worker, "Run", Qt::QueuedConnection); + QTest::qWait(100); + QVERIFY(m_testThread->isRunning()); + + QMetaObject::invokeMethod(m_worker, "Stop", Qt::QueuedConnection); + QTest::qWait(100); + } + QVERIFY(true); +} \ No newline at end of file diff --git a/src/backend/unit_tests/backend_worker_test/backend_worker_test.h b/src/backend/unit_tests/backend_worker_test/backend_worker_test.h new file mode 100644 index 0000000..a3251e1 --- /dev/null +++ b/src/backend/unit_tests/backend_worker_test/backend_worker_test.h @@ -0,0 +1,20 @@ +#include + +class BackendWorkerTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + + void isStart(); + void doubleStart(); + void isStop(); + void doubleStop(); + void moveOnNextStage(); + void correctCurrentStatus(); + void isFinishedReport(); + void isFinishedHistory(); + void isAlarmEvent(); + void isWarningEvent(); + void isProcessFinished(); +}; // BackendWorkerTest diff --git a/src/backend/unit_tests/data_processor_test/data_processor_test.cpp b/src/backend/unit_tests/data_processor_test/data_processor_test.cpp new file mode 100644 index 0000000..791d6b2 --- /dev/null +++ b/src/backend/unit_tests/data_processor_test/data_processor_test.cpp @@ -0,0 +1,421 @@ +#include +#include +#include "../../../include/backend/data_processing/DataProcessor.h" +#include "../../../include/backend/DataTypes.h" + +class DataProcessorTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + + // Тесты конструктора + void testConstructor(); + + // Тесты обработки кадров + void testProcessFrameNormal(); + void testProcessFrameRpmOverspeed(); + void testProcessFrameDieselOverheat(); + void testProcessFrameMotorOverheat(); + void testProcessFrameResistorOverheat(); + void testProcessFramePressureOver(); + void testProcessFramePressureUnder(); + + // Тесты предупреждений + void testPreWarningRpmHigh(); + void testPreWarningDieselTempHigh(); + void testPreWarningMotorTempHigh(); + void testPreWarningResistorHigh(); + void testPreWarningPressureHigh(); + void testPreWarningPressureLow(); + + // Тесты смены этапов + void testStageChange(); + void testDifferentStages(); + + // Тест сброса + void testReset(); + void testAlarmLatch(); + + // Тест конфигурации + void testConfigChange(); + + // Тесты сигналов + void testDecisionReadySignal(); + + // Тесты на граничных значениях + void testBoundaryValues(); + void testInvalidFrames(); + +private: + DataProcessor* m_processor; + ModelConfig m_config; +}; + +void DataProcessorTest::init() +{ + m_config.maxRpm = 3000; + m_config.maxDieselTemp = 120; + m_config.maxMotorTemp = 100; + m_config.maxResistorBalance = 150; + m_config.maxDieselPressure = 10.0; + m_config.minDieselPressure = 2.0; + + m_processor = new DataProcessor(m_config); +} + +void DataProcessorTest::cleanup() +{ + delete m_processor; +} + +void DataProcessorTest::testConstructor() +{ + DataProcessor* processor = new DataProcessor(m_config); + QVERIFY(processor != nullptr); + delete processor; +} + +void DataProcessorTest::testProcessFrameNormal() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2000; + frame.dieselTemp = 90; + frame.motorTemp = 80; + frame.resistorBalance = 100; + frame.dieselPressure = 5.0; + frame.stage = 3; // HOT_NO_LOAD + + m_processor->onStageChanged(3); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Ok); +} + +void DataProcessorTest::testProcessFrameRpmOverspeed() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 3500; // Превышение (max 3000) + frame.dieselTemp = 90; + frame.motorTemp = 80; + frame.resistorBalance = 100; + frame.dieselPressure = 5.0; + frame.stage = 3; + + m_processor->onStageChanged(3); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_RpmOverspeed); + QVERIFY(!decision.reason.isEmpty()); + QVERIFY(decision.controls.size() > 0); + + // Проверяем, что есть EmergencyStop + bool hasEmergencyStop = false; + for(const auto& control : decision.controls) { + if(control.type == ControlType::EmergencyStop) { + hasEmergencyStop = true; + break; + } + } + QVERIFY(hasEmergencyStop); +} + +void DataProcessorTest::testProcessFrameDieselOverheat() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2000; + frame.dieselTemp = 150; // Превышение (max 120) + frame.motorTemp = 80; + frame.resistorBalance = 100; + frame.dieselPressure = 5.0; + frame.stage = 3; + + m_processor->onStageChanged(3); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_DieselOverheat); +} + +void DataProcessorTest::testProcessFrameMotorOverheat() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2000; + frame.dieselTemp = 90; + frame.motorTemp = 120; // Превышение (max 100) + frame.resistorBalance = 100; + frame.dieselPressure = 5.0; + + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_MotorOverheat); +} + +void DataProcessorTest::testProcessFrameResistorOverheat() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2000; + frame.dieselTemp = 90; + frame.motorTemp = 80; + frame.resistorBalance = 180; // Превышение (max 150) + frame.dieselPressure = 5.0; + frame.stage = 4; // HOT_WITH_LOAD + + m_processor->onStageChanged(4); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_ResistorOverheat); +} + +void DataProcessorTest::testProcessFramePressureOver() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2000; + frame.dieselTemp = 90; + frame.motorTemp = 80; + frame.resistorBalance = 100; + frame.dieselPressure = 12.0; // Превышение (max 10.0) + frame.stage = 2; // START_AND_WARMUP + + m_processor->onStageChanged(2); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_PressureOver); +} + +void DataProcessorTest::testProcessFramePressureUnder() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2000; + frame.dieselTemp = 90; + frame.motorTemp = 80; + frame.resistorBalance = 100; + frame.dieselPressure = 1.0; // Ниже минимума (min 2.0) + frame.stage = 2; + + m_processor->onStageChanged(2); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_PressureUnder); +} + +void DataProcessorTest::testPreWarningRpmHigh() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2750; // 90% от 3000 + frame.stage = 3; + + m_processor->onStageChanged(3); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::PreWarn_RpmHigh); +} + +void DataProcessorTest::testPreWarningDieselTempHigh() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.dieselTemp = 108; // 90% от 120 + frame.stage = 3; + + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::PreWarn_DieselTempHigh); +} + +void DataProcessorTest::testPreWarningMotorTempHigh() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.motorTemp = 90; // 90% от 100 + frame.stage = 3; + + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::PreWarn_MotorTempHigh); +} + +void DataProcessorTest::testStageChange() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 3500; // Вызовет аварию + + m_processor->onStageChanged(1); // COLD_CRANKING + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_RpmOverspeed); +} + +void DataProcessorTest::testDifferentStages() +{ + QVector stages = {1, 2, 3, 4, 5, 6}; + SensorFrame normalFrame; + normalFrame.rpm = 2000; + normalFrame.dieselTemp = 90; + normalFrame.motorTemp = 80; + + for(int stage : stages) { + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + m_processor->onStageChanged(stage); + m_processor->processFrame(normalFrame); + + // В неактивных этапах не должно быть проверок + if(stage == 0 || stage == 5 || stage == 6) { + QCOMPARE(spy.count(), 1); + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Ok); + } + } +} + +void DataProcessorTest::testReset() +{ + SensorFrame alarmFrame; + alarmFrame.rpm = 3500; + alarmFrame.stage = 3; + + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + m_processor->onStageChanged(3); + m_processor->processFrame(alarmFrame); + QCOMPARE(spy.count(), 1); + + // После аварии новые фреймы не должны обрабатываться + m_processor->processFrame(alarmFrame); + QCOMPARE(spy.count(), 1); // Количество не изменилось + + m_processor->reset(); + + // После сброса должно снова обрабатывать + m_processor->processFrame(alarmFrame); + QCOMPARE(spy.count(), 2); // Увеличилось +} + +void DataProcessorTest::testAlarmLatch() +{ + SensorFrame alarmFrame; + alarmFrame.rpm = 3500; + alarmFrame.stage = 3; + + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + m_processor->onStageChanged(3); + m_processor->processFrame(alarmFrame); + QCOMPARE(spy.count(), 1); + + SensorFrame normalFrame; + normalFrame.rpm = 2000; + normalFrame.stage = 3; + + m_processor->processFrame(normalFrame); + QCOMPARE(spy.count(), 1); // Не увеличилось - защелка аварии +} + +void DataProcessorTest::testConfigChange() +{ + ModelConfig newConfig; + newConfig.maxRpm = 4000; + newConfig.maxDieselTemp = 150; + + m_processor->onConfigChanged(newConfig); + + SensorFrame frame; + frame.rpm = 3800; // Было бы аварией со старой конфигурацией + frame.stage = 3; + + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + m_processor->onStageChanged(3); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Ok); // С новой конфигурацией - норма +} + +void DataProcessorTest::testDecisionReadySignal() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + SensorFrame frame; + frame.rpm = 2000; + frame.stage = 3; + + m_processor->onStageChanged(3); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + QVERIFY(spy.isValid()); +} + +void DataProcessorTest::testBoundaryValues() +{ + QSignalSpy spy(m_processor, &DataProcessor::decisionReady); + + // Точные граничные значения + SensorFrame frame; + frame.rpm = 3000; // Точно на границе + frame.dieselTemp = 120; // Точно на границе + frame.motorTemp = 100; // Точно на границе + frame.resistorBalance = 150; // Точно на границе + frame.dieselPressure = 10.0; // Точно на границе + frame.stage = 4; + + m_processor->onStageChanged(4); + m_processor->processFrame(frame); + + QCOMPARE(spy.count(), 1); + Decision decision = spy.takeFirst().at(0).value(); + QCOMPARE(decision.state, DiagState::Alarm_RpmOverspeed); // Должна быть авария +} \ No newline at end of file diff --git a/src/backend/unit_tests/data_processor_test/data_processor_test.h b/src/backend/unit_tests/data_processor_test/data_processor_test.h new file mode 100644 index 0000000..b163a0a --- /dev/null +++ b/src/backend/unit_tests/data_processor_test/data_processor_test.h @@ -0,0 +1,13 @@ +#include + +class DataProcessorTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + void correctFrame(); + void isDecisionReady(); + void isWarningRaised(); + void isAlarmRaised(); + void isControlProduced(); +}; // DataProcessorTest diff --git a/src/backend/unit_tests/data_store_test/data_store_test.cpp b/src/backend/unit_tests/data_store_test/data_store_test.cpp new file mode 100644 index 0000000..d58a507 --- /dev/null +++ b/src/backend/unit_tests/data_store_test/data_store_test.cpp @@ -0,0 +1,323 @@ +#include +#include +#include +#include +#include "../../../include/backend/data_store/DataStore.h" + +class DataStoreTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + + // Тесты конструктора + void testConstructor(); + + // Тесты записи + void testWriteRecord(); + void testWriteEvent(); + void testMultipleWriteRecord(); + void testMultipleWriteEvent(); + + // Тесты чтения + void testReadRecords(); + void testReadEvents(); + void testReadNonExistent(); + + // Тесты отчета + void testBuildReportCompleted(); + void testBuildReportAborted(); + + // Тесты работы с файлами + void testFileCreation(); + void testHeadersWritten(); + void testEmptyFileRead(); + + // Тесты граничных случаев + void testLargeData(); + void testConcurrentWrites(); + +private: + QTemporaryDir m_tempDir; + CSVConnector* m_measurementConnector; + CSVConnector* m_eventConnector; + DataStore* m_dataStore; + QString m_measurementPath; + QString m_eventPath; +}; + +void DataStoreTest::init() +{ + QVERIFY(m_tempDir.isValid()); + m_measurementPath = m_tempDir.path() + "/measurements.csv"; + m_eventPath = m_tempDir.path() + "/events.csv"; + + m_measurementConnector = new CSVConnector(m_measurementPath.toStdString()); + m_eventConnector = new CSVConnector(m_eventPath.toStdString()); + m_dataStore = new DataStore(m_measurementConnector, m_eventConnector); +} + +void DataStoreTest::cleanup() +{ + delete m_dataStore; + delete m_measurementConnector; + delete m_eventConnector; + + // Очищаем временные файлы + QFile::remove(m_measurementPath); + QFile::remove(m_eventPath); +} + +void DataStoreTest::testConstructor() +{ + DataStore* store = new DataStore(m_measurementConnector, m_eventConnector); + QVERIFY(store != nullptr); + delete store; +} + +void DataStoreTest::testWriteRecord() +{ + MeasurementRecord record; + record.runId = 1; + record.stage = 1; + record.timestampMs = QDateTime::currentMSecsSinceEpoch(); + record.rpm = 1500; + record.torque = 100; + record.dieselTemp = 85; + record.motorTemp = 75; + record.resistorTemp = 60; + record.dieselPressure = 5.0; + record.throttle = 50; + record.brakeTorque = 30; + record.flags = "OK"; + + m_dataStore->writeRecord(record); + QTest::qWait(100); + + QVector records = m_dataStore->readRecords(1); + QCOMPARE(records.size(), 1); + QCOMPARE(records[0].runId, record.runId); + QCOMPARE(records[0].rpm, record.rpm); +} + +void DataStoreTest::testWriteEvent() +{ + EventRecord event; + event.runId = 1; + event.timestampMs = QDateTime::currentMSecsSinceEpoch(); + event.stage = 1; + event.type = "stage_change"; + event.message = "Entered stage 1"; + + m_dataStore->writeEvent(event); + QTest::qWait(100); + + QVector events = m_dataStore->readEvents(1); + QCOMPARE(events.size(), 1); + QCOMPARE(events[0].type, event.type); + QCOMPARE(events[0].message, event.message); +} + +void DataStoreTest::testMultipleWriteRecord() +{ + const int recordCount = 10; + + for(int i = 0; i < recordCount; ++i) { + MeasurementRecord record; + record.runId = 1; + record.stage = i; + record.timestampMs = QDateTime::currentMSecsSinceEpoch(); + record.rpm = i * 100; + m_dataStore->writeRecord(record); + } + + QTest::qWait(200); + + QVector records = m_dataStore->readRecords(1); + QCOMPARE(records.size(), recordCount); + + for(int i = 0; i < recordCount; ++i) { + QCOMPARE(records[i].stage, i); + QCOMPARE(records[i].rpm, i * 100); + } +} + +void DataStoreTest::testMultipleWriteEvent() +{ + const int eventCount = 10; + + for(int i = 0; i < eventCount; ++i) { + EventRecord event; + event.runId = 1; + event.timestampMs = QDateTime::currentMSecsSinceEpoch(); + event.stage = i; + event.type = "test"; + event.message = QString("Event %1").arg(i); + m_dataStore->writeEvent(event); + } + + QTest::qWait(200); + + QVector events = m_dataStore->readEvents(1); + QCOMPARE(events.size(), eventCount); +} + +void DataStoreTest::testReadRecords() +{ + // Записываем данные + MeasurementRecord record; + record.runId = 42; + record.rpm = 2000; + m_dataStore->writeRecord(record); + + QTest::qWait(100); + + // Читаем данные + QVector records = m_dataStore->readRecords(42); + QCOMPARE(records.size(), 1); + QCOMPARE(records[0].runId, 42); + QCOMPARE(records[0].rpm, 2000); +} + +void DataStoreTest::testReadEvents() +{ + EventRecord event; + event.runId = 42; + event.type = "test_event"; + m_dataStore->writeEvent(event); + + QTest::qWait(100); + + QVector events = m_dataStore->readEvents(42); + QCOMPARE(events.size(), 1); + QCOMPARE(events[0].type, "test_event"); +} + +void DataStoreTest::testReadNonExistent() +{ + QVector records = m_dataStore->readRecords(999); + QVERIFY(records.isEmpty()); + + QVector events = m_dataStore->readEvents(999); + QVERIFY(events.isEmpty()); +} + +void DataStoreTest::testBuildReportCompleted() +{ + // Записываем тестовые данные для завершенного испытания + for(int i = 0; i < 5; ++i) { + MeasurementRecord record; + record.runId = 100; + record.stage = i; + record.timestampMs = QDateTime::currentMSecsSinceEpoch(); + m_dataStore->writeRecord(record); + + EventRecord event; + event.runId = 100; + event.stage = i; + event.type = "stage_change"; + event.message = QString("Stage %1 completed").arg(i); + m_dataStore->writeEvent(event); + } + + EventRecord finalEvent; + finalEvent.runId = 100; + finalEvent.type = "completed"; + finalEvent.message = "Test completed successfully"; + m_dataStore->writeEvent(finalEvent); + + QTest::qWait(200); + + Report report = m_dataStore->buildReport(100); + QCOMPARE(report.runId, 100); + QCOMPARE(report.finalStatus, "completed"); + QVERIFY(report.stages.size() > 0); + QVERIFY(report.events.size() > 0); +} + +void DataStoreTest::testBuildReportAborted() +{ + EventRecord abortEvent; + abortEvent.runId = 101; + abortEvent.type = "abort"; + abortEvent.message = "Test aborted due to alarm"; + m_dataStore->writeEvent(abortEvent); + + QTest::qWait(100); + + Report report = m_dataStore->buildReport(101); + QCOMPARE(report.runId, 101); + QCOMPARE(report.finalStatus, "aborted"); +} + +void DataStoreTest::testFileCreation() +{ + // Проверяем, что файлы создаются + QVERIFY(QFile::exists(m_measurementPath)); + QVERIFY(QFile::exists(m_eventPath)); + + // Проверяем, что файлы не пустые (есть заголовки) + QFile measFile(m_measurementPath); + measFile.open(QIODevice::ReadOnly); + QVERIFY(measFile.size() > 0); + measFile.close(); +} + +void DataStoreTest::testHeadersWritten() +{ + // Проверяем наличие заголовков в CSV + QFile measFile(m_measurementPath); + measFile.open(QIODevice::ReadOnly); + QString firstLine = measFile.readLine(); + measFile.close(); + + QVERIFY(firstLine.contains("runId") || firstLine.contains("timestamp")); +} + +void DataStoreTest::testEmptyFileRead() +{ + QVector records = m_dataStore->readRecords(999); + QVERIFY(records.isEmpty()); +} + +void DataStoreTest::testLargeData() +{ + const int largeCount = 1000; + + for(int i = 0; i < largeCount; ++i) { + MeasurementRecord record; + record.runId = 200; + record.stage = i % 5; + record.rpm = i; + m_dataStore->writeRecord(record); + } + + QTest::qWait(1000); + + QVector records = m_dataStore->readRecords(200); + QCOMPARE(records.size(), largeCount); + + // Проверяем, что данные корректны + for(int i = 0; i < largeCount; ++i) { + QCOMPARE(records[i].rpm, i); + } +} + +void DataStoreTest::testConcurrentWrites() +{ + // Тест одновременной записи разных runId + for(int runId = 0; runId < 10; ++runId) { + MeasurementRecord record; + record.runId = runId; + record.rpm = runId * 100; + m_dataStore->writeRecord(record); + } + + QTest::qWait(200); + + for(int runId = 0; runId < 10; ++runId) { + QVector records = m_dataStore->readRecords(runId); + QCOMPARE(records.size(), 1); + QCOMPARE(records[0].rpm, runId * 100); + } +} \ No newline at end of file diff --git a/src/backend/unit_tests/data_store_test/data_store_test.h b/src/backend/unit_tests/data_store_test/data_store_test.h new file mode 100644 index 0000000..3f7fe67 --- /dev/null +++ b/src/backend/unit_tests/data_store_test/data_store_test.h @@ -0,0 +1,21 @@ +#include + +class DataStoreTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + void isFileExist(); + void isWriteRecord(); + void correctWriteRecord(); + void doubleWriteRecord(); + void rewritePrevRecord(); + void isWriteEvent(); + void correctWriteEvent(); + void doubleWriteEvent(); + void rewritePrevEvent(); + void isReadRecord(); + void correctReadRecord(); + void isReadEvent(); + void correctReadEvent(); +}; // DataStoreTest diff --git a/src/backend/unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.cpp b/src/backend/unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.cpp new file mode 100644 index 0000000..f446732 --- /dev/null +++ b/src/backend/unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.cpp @@ -0,0 +1,272 @@ +#include +#include +#include +#include "../../../include/backend/modbus_client/IModbusBridge.h" +#include "../../../include/backend/modbus_client/MockModbusBridge.h" +#include "../../../include/backend/modbus_client/QtModbusBridge.h" +#include "../../../include/backend/DataTypes.h" + +class ModbusDataBridgeTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + + // Тесты Mock реализации + void testMockConstructor(); + void testMockStartPolling(); + void testMockStopPolling(); + void testMockOnReadSensors(); + void testMockOnReadInfo(); + void testMockOnWriteConfig(); + void testMockSignals(); + + // Тесты Qt реализации (если доступна) + void testQtConstructor(); + void testQtStartPolling(); + void testQtStopPolling(); + void testQtOnReadSensors(); + void testQtConnectionErrors(); + + // Общие тесты интерфейса + void testInterfaceCompliance(); + void testMultipleStartStop(); + void testRapidPolling(); + +private: + ModbusConfig m_config; + MockModbusBridge* m_mockBridge; +}; + +void ModbusDataBridgeTest::init() +{ + m_config.host = "127.0.0.1"; + m_config.port = 1502; + m_config.pollFrequencyMs = 100; + m_config.timeoutMs = 1000; + m_config.retries = 3; + m_config.unitId = 1; + + m_mockBridge = new MockModbusBridge(m_config); +} + +void ModbusDataBridgeTest::cleanup() +{ + delete m_mockBridge; +} + +void ModbusDataBridgeTest::testMockConstructor() +{ + MockModbusBridge* bridge = new MockModbusBridge(m_config); + QVERIFY(bridge != nullptr); + delete bridge; +} + +void ModbusDataBridgeTest::testMockStartPolling() +{ + QSignalSpy dataSpy(m_mockBridge, &IModbusBridge::sensorsDataReady); + + m_mockBridge->startPolling(); + QTest::qWait(250); // Ждем минимум 2 цикла опроса + + QVERIFY(dataSpy.count() >= 2); +} + +void ModbusDataBridgeTest::testMockStopPolling() +{ + m_mockBridge->startPolling(); + QTest::qWait(150); + + QSignalSpy dataSpy(m_mockBridge, &IModbusBridge::sensorsDataReady); + int countBeforeStop = dataSpy.count(); + + m_mockBridge->stopPolling(); + QTest::qWait(250); + + int countAfterStop = dataSpy.count(); + // После остановки данные не должны приходить + QCOMPARE(countAfterStop, countBeforeStop); +} + +void ModbusDataBridgeTest::testMockOnReadSensors() +{ + QSignalSpy dataSpy(m_mockBridge, &IModbusBridge::sensorsDataReady); + + m_mockBridge->onReadSensors(); + QTest::qWait(50); + + QCOMPARE(dataSpy.count(), 1); + + if(dataSpy.count() > 0) { + SensorFrame frame = dataSpy.first().at(0).value(); + // Проверяем, что данные валидны + QVERIFY(frame.rpm >= 0); + QVERIFY(frame.dieselTemp >= 0); + } +} + +void ModbusDataBridgeTest::testMockOnReadInfo() +{ + QSignalSpy infoSpy(m_mockBridge, &IModbusBridge::modelInfoReady); + + m_mockBridge->onReadInfo(); + QTest::qWait(50); + + QCOMPARE(infoSpy.count(), 1); +} + +void ModbusDataBridgeTest::testMockOnWriteConfig() +{ + ModelConfig config; + config.maxRpm = 3000; + + // Проверяем, что запись не вызывает ошибок + m_mockBridge->onWriteConfig(config); + QVERIFY(true); +} + +void ModbusDataBridgeTest::testMockSignals() +{ + QSignalSpy dataSpy(m_mockBridge, &IModbusBridge::sensorsDataReady); + QSignalSpy infoSpy(m_mockBridge, &IModbusBridge::modelInfoReady); + + m_mockBridge->startPolling(); + QTest::qWait(200); + m_mockBridge->stopPolling(); + + QVERIFY(dataSpy.count() > 0); + QVERIFY(infoSpy.count() == 0); // info не запрашивалась автоматически +} + +void ModbusDataBridgeTest::testQtConstructor() +{ +#ifdef QT_MODBUS_LIB + QtModbusBridge* bridge = new QtModbusBridge(m_config); + QVERIFY(bridge != nullptr); + delete bridge; +#else + QSKIP("QtModbusBridge not available"); +#endif +} + +void ModbusDataBridgeTest::testQtStartPolling() +{ +#ifdef QT_MODBUS_LIB + QtModbusBridge* bridge = new QtModbusBridge(m_config); + QSignalSpy dataSpy(bridge, &IModbusBridge::sensorsDataReady); + + bridge->startPolling(); + QTest::qWait(250); + + // Может быть 0 если сервер не доступен + QVERIFY(dataSpy.count() >= 0); + delete bridge; +#else + QSKIP("QtModbusBridge not available"); +#endif +} + +void ModbusDataBridgeTest::testQtStopPolling() +{ +#ifdef QT_MODBUS_LIB + QtModbusBridge* bridge = new QtModbusBridge(m_config); + bridge->startPolling(); + QTest::qWait(150); + + QSignalSpy dataSpy(bridge, &IModbusBridge::sensorsDataReady); + int countBeforeStop = dataSpy.count(); + + bridge->stopPolling(); + QTest::qWait(250); + + int countAfterStop = dataSpy.count(); + QCOMPARE(countAfterStop, countBeforeStop); + delete bridge; +#else + QSKIP("QtModbusBridge not available"); +#endif +} + +void ModbusDataBridgeTest::testQtOnReadSensors() +{ +#ifdef QT_MODBUS_LIB + QtModbusBridge* bridge = new QtModbusBridge(m_config); + QSignalSpy dataSpy(bridge, &IModbusBridge::sensorsDataReady); + + bridge->onReadSensors(); + QTest::qWait(100); + + // Может быть 0 если сервер не доступен + QVERIFY(dataSpy.count() >= 0); + delete bridge; +#else + QSKIP("QtModbusBridge not available"); +#endif +} + +void ModbusDataBridgeTest::testQtConnectionErrors() +{ +#ifdef QT_MODBUS_LIB + ModbusConfig invalidConfig; + invalidConfig.host = "192.168.255.255"; // Несуществующий хост + invalidConfig.port = 9999; + + QtModbusBridge* bridge = new QtModbusBridge(invalidConfig); + + QSignalSpy connectionErrorSpy(bridge, &IModbusBridge::connectionError); + QSignalSpy connectionLostSpy(bridge, &IModbusBridge::connectionLost); + + // Ждем ошибок подключения + QTest::qWait(500); + + // Должны быть ошибки + QVERIFY(connectionErrorSpy.count() >= 0 || connectionLostSpy.count() >= 0); + delete bridge; +#else + QSKIP("QtModbusBridge not available"); +#endif +} + +void ModbusDataBridgeTest::testInterfaceCompliance() +{ + // Проверяем, что Mock реализует весь интерфейс + IModbusBridge* bridge = m_mockBridge; + + // Все методы должны быть доступны + bridge->startPolling(); + bridge->stopPolling(); + bridge->onReadSensors(); + bridge->onReadInfo(); + bridge->onWriteConfig(ModelConfig()); + + QVERIFY(true); +} + +void ModbusDataBridgeTest::testMultipleStartStop() +{ + for(int i = 0; i < 5; ++i) { + m_mockBridge->startPolling(); + QTest::qWait(100); + m_mockBridge->stopPolling(); + QTest::qWait(50); + } + QVERIFY(true); +} + +void ModbusDataBridgeTest::testRapidPolling() +{ + ModbusConfig fastConfig = m_config; + fastConfig.pollFrequencyMs = 10; // Очень быстрый опрос + + MockModbusBridge* fastBridge = new MockModbusBridge(fastConfig); + QSignalSpy dataSpy(fastBridge, &IModbusBridge::sensorsDataReady); + + fastBridge->startPolling(); + QTest::qWait(200); // Должно быть ~20 измерений + + QVERIFY(dataSpy.count() >= 10); + QVERIFY(dataSpy.count() <= 30); + + fastBridge->stopPolling(); + delete fastBridge; +} \ No newline at end of file diff --git a/src/backend/unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.h b/src/backend/unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.h new file mode 100644 index 0000000..9f71b43 --- /dev/null +++ b/src/backend/unit_tests/modbus_data_bridge_test/modbus_data_bridge_test.h @@ -0,0 +1,17 @@ +#include + +class ModbusDataBridgeTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + void isStartPolling(); + void doubleStartPolling(); + void isStopPolling(); + void doubleStopPolling(); + void isWriteCommand(); + void correctWriteCommand(); + void isReadData(); + void correctReadData(); + void correctReadError(); +}; // ModbusDataBridgeTest diff --git a/src/backend/unit_tests/state_machine_test/state_machine_test.cpp b/src/backend/unit_tests/state_machine_test/state_machine_test.cpp new file mode 100644 index 0000000..021c294 --- /dev/null +++ b/src/backend/unit_tests/state_machine_test/state_machine_test.cpp @@ -0,0 +1,265 @@ +#include +#include +#include +#include "../../../include/backend/state_machine.h" +#include "../../../include/backend/backend_worker/backendworker.h" +#include "../../../include/backend/DataTypes.h" + +class StateMachineTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + + // Тесты конструктора + void testConstructor(); + + // Тесты состояний + void testInitialState(); + void testTransitionTo(); + + // Тесты этапов + void testRequestStart(); + void testRequestNextStage(); + void testRequestAbort(); + void testStageSequence(); + + // Тесты сигналов + void testStageChangedSignal(); + void testFinishedSignal(); + + // Тесты таймеров + void testStageTimeout(); + + // Тесты обработки команд + void testOnReceivedFrontControl(); + void testOnReceivedModelConfig(); + + // Тесты интеграции с Modbus + void testModbusBridgeIntegration(); + + // Тесты граничных случаев + void testInvalidStageTransition(); + void testAbortDuringStage(); + +private: + BackendWorker* m_backendWorker; + StateMachine* m_stateMachine; + QThread* m_workerThread; +}; + +void StateMachineTest::init() +{ + m_backendWorker = new BackendWorker(); + m_workerThread = new QThread(); + m_backendWorker->moveToThread(m_workerThread); + m_workerThread->start(); + + m_stateMachine = new StateMachine(m_backendWorker); +} + +void StateMachineTest::cleanup() +{ + delete m_stateMachine; + m_workerThread->quit(); + m_workerThread->wait(); + delete m_backendWorker; + delete m_workerThread; +} + +void StateMachineTest::testConstructor() +{ + StateMachine* sm = new StateMachine(m_backendWorker); + QVERIFY(sm != nullptr); + QCOMPARE(sm->currentStageIndex(), -1); + delete sm; +} + +void StateMachineTest::testInitialState() +{ + QCOMPARE(m_stateMachine->currentStageIndex(), -1); + QCOMPARE(m_stateMachine->currentState(), DiagState::Ok); +} + +void StateMachineTest::testTransitionTo() +{ + QSignalSpy stageSpy(m_stateMachine, &StateMachine::stageChanged); + + ModelConfig config; + m_stateMachine->requestStart(config); + + QTest::qWait(100); + + // Проверяем, что состояние изменилось + QVERIFY(stageSpy.count() >= 0); +} + +void StateMachineTest::testRequestStart() +{ + QSignalSpy finishedSpy(m_stateMachine, &StateMachine::finished); + + ModelConfig config; + config.maxRpm = 3000; + config.maxDieselTemp = 120; + + m_stateMachine->requestStart(config); + QTest::qWait(100); + + // Проверяем, что процесс запустился + QVERIFY(m_stateMachine->currentStageIndex() >= 0); +} + +void StateMachineTest::testRequestNextStage() +{ + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + int oldStage = m_stateMachine->currentStageIndex(); + m_stateMachine->requestNextStage(); + QTest::qWait(100); + + int newStage = m_stateMachine->currentStageIndex(); + QVERIFY(newStage != oldStage || newStage == -1); +} + +void StateMachineTest::testRequestAbort() +{ + QSignalSpy finishedSpy(m_stateMachine, &StateMachine::finished); + + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + m_stateMachine->requestAbort("Test abort"); + QTest::qWait(100); + + QCOMPARE(finishedSpy.count(), 1); +} + +void StateMachineTest::testStageSequence() +{ + QSignalSpy stageSpy(m_stateMachine, &StateMachine::stageChanged); + + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + // Проходим по всем этапам + for(int i = 0; i < 10; ++i) { + m_stateMachine->requestNextStage(); + QTest::qWait(50); + } + + // Проверяем, что сигналы смены этапов были + QVERIFY(stageSpy.count() > 0); +} + +void StateMachineTest::testStageChangedSignal() +{ + QSignalSpy spy(m_stateMachine, &StateMachine::stageChanged); + + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + QVERIFY(spy.count() >= 0); + + if(spy.count() > 0) { + QList arguments = spy.first(); + QCOMPARE(arguments.size(), 2); // oldStage, newStage + } +} + +void StateMachineTest::testFinishedSignal() +{ + QSignalSpy spy(m_stateMachine, &StateMachine::finished); + + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + m_stateMachine->requestAbort("Force abort"); + QTest::qWait(100); + + QCOMPARE(spy.count(), 1); + + if(spy.count() > 0) { + QList arguments = spy.first(); + QCOMPARE(arguments.size(), 1); // finalState + } +} + +void StateMachineTest::testStageTimeout() +{ + // Тест автоматического перехода по таймауту + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(500); // Ждем возможного таймаута + + // Проверяем, что этапы переключаются автоматически + // (зависит от реализации) +} + +void StateMachineTest::testOnReceivedFrontControl() +{ + FrontControl control; + // control.control = ControlType::NextStage; + + QSignalSpy stageSpy(m_stateMachine, &StateMachine::stageChanged); + + // Эмулируем получение команды через коммуникатор + // (зависит от реализации) +} + +void StateMachineTest::testOnReceivedModelConfig() +{ + ModelConfig config; + config.maxRpm = 5000; + config.maxDieselTemp = 200; + + // Эмулируем получение конфигурации + // (зависит от реализации) +} + +void StateMachineTest::testModbusBridgeIntegration() +{ + // Проверяем, что ModbusBridge правильно инициализирован + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + // Проверяем состояние Modbus клиента + // (зависит от реализации) +} + +void StateMachineTest::testInvalidStageTransition() +{ + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + // Попытка перейти на следующий этап когда он не должен быть доступен + for(int i = 0; i < 20; ++i) { + m_stateMachine->requestNextStage(); + QTest::qWait(50); + } + + // Не должно быть краха + QVERIFY(true); +} + +void StateMachineTest::testAbortDuringStage() +{ + QSignalSpy finishedSpy(m_stateMachine, &StateMachine::finished); + + ModelConfig config; + m_stateMachine->requestStart(config); + QTest::qWait(100); + + // Прерываем на середине этапа + m_stateMachine->requestAbort("Abort during stage"); + QTest::qWait(100); + + QCOMPARE(finishedSpy.count(), 1); +} \ No newline at end of file diff --git a/src/backend/unit_tests/state_machine_test/state_machine_test.h b/src/backend/unit_tests/state_machine_test/state_machine_test.h new file mode 100644 index 0000000..b326e9a --- /dev/null +++ b/src/backend/unit_tests/state_machine_test/state_machine_test.h @@ -0,0 +1,12 @@ +#include + +class StateMachineTest : public QObject { + Q_OBJECT +private slots: + void init(); + void cleanup(); + void isStart(); + void moveOnNextStage(); + void isAbort(); + void correctUIStatus(); +}; // StateMachineTest