From c1223443e8ede41c263b1097fe1c41188bb7715e Mon Sep 17 00:00:00 2001 From: KraPete <86825564+KraPete@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:42:32 +0500 Subject: [PATCH] Add Modbus protocol support to port/Load RPC (#805) --- README.md | 58 ++ debian/changelog | 6 + src/devices/modbus_device.cpp | 2 - src/devices/modbus_device.h | 1 + src/devices/modbus_io_device.cpp | 2 - src/modbus_base.cpp | 457 ++++++++++++ src/modbus_base.h | 179 +++++ src/modbus_common.cpp | 696 ++++-------------- src/modbus_common.h | 134 +--- src/modbus_ext_common.cpp | 86 ++- src/modbus_ext_common.h | 28 +- src/rpc/rpc_port_handler.cpp | 16 +- src/rpc/rpc_port_handler.h | 2 - src/rpc/rpc_port_load_handler.cpp | 155 +--- src/rpc/rpc_port_load_handler.h | 13 +- ...pc_port_load_modbus_serial_client_task.cpp | 116 +++ .../rpc_port_load_modbus_serial_client_task.h | 36 + .../rpc_port_load_raw_serial_client_task.cpp | 70 ++ .../rpc_port_load_raw_serial_client_task.h | 33 + src/rpc/rpc_port_load_request.cpp | 81 ++ src/rpc/rpc_port_load_request.h | 17 +- src/rpc/rpc_port_load_serial_client_task.cpp | 46 -- src/rpc/rpc_port_load_serial_client_task.h | 20 - src/rpc/rpc_port_setup_handler.cpp | 23 +- src/rpc/rpc_port_setup_handler.h | 2 +- src/rpc/rpc_port_setup_serial_client_task.cpp | 16 +- src/serial_client_events_reader.cpp | 27 +- src/serial_client_events_reader.h | 1 + test/modbus_ext_common_test.cpp | 87 +-- test/modbus_tcp_test.cpp | 91 +-- test/serial_client_test.cpp | 11 +- ...t-serial-rpc-port-load-request.schema.json | 129 ++-- 32 files changed, 1504 insertions(+), 1137 deletions(-) create mode 100644 src/modbus_base.cpp create mode 100644 src/modbus_base.h create mode 100644 src/rpc/rpc_port_load_modbus_serial_client_task.cpp create mode 100644 src/rpc/rpc_port_load_modbus_serial_client_task.h create mode 100644 src/rpc/rpc_port_load_raw_serial_client_task.cpp create mode 100644 src/rpc/rpc_port_load_raw_serial_client_task.h create mode 100644 src/rpc/rpc_port_load_request.cpp delete mode 100644 src/rpc/rpc_port_load_serial_client_task.cpp delete mode 100644 src/rpc/rpc_port_load_serial_client_task.h diff --git a/README.md b/README.md index 92e3e65b5..7ed4f18e6 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ It's designed to be used on [Wiren Board](https://wirenboard.com/en/) family of - [Диаграмма таймаутов цикла опроса](#диаграмма-таймаутов-цикла-опроса) - [Объединенное чтение регистров и его авто-отключение](#объединенное-чтение-регистров-и-его-авто-отключение) - [Прямое чтение и запись в порт](#прямое-чтение-и-запись-в-порт) + - [Чтение и запись по протоколу Modbus](#чтение-и-запись-по-протоколу-modbus) - [Протоколы](#протоколы) - [Поддержка различных протоколов на одной шине](#поддержка-различных-протоколов-на-одной-шине) - [Широковещательные сообщения](#широковещательные-сообщения) @@ -963,6 +964,63 @@ It's designed to be used on [Wiren Board](https://wirenboard.com/en/) family of RPC Client <- {"error":{"code":-32600,"data":"Request handler is not responding @ src/rpc_handler.cpp:179","message":"Request timeout"},"id":1,"result":null} ``` +### Чтение и запись по протоколу Modbus + +Существует возможность выполнить запись и чтение регистров произвольного устройства по протоколу Modbus посредством MQTT RPC запроса. Выполнение запроса встраивается в цикл опроса устройств таким образом, что запрос выполнится с высоким приоритетом сразу после окончания текущего цикла опроса. +Для упрощенного использования данного функционала написана [Python-библиотека](https://github.com/wirenboard/python-mqtt-rpc/). Также по [ссылке](https://github.com/wirenboard/modbus-utils-rpc) доступна утилита для работы с modbus-устройствами при помощи RPC-функционала wb-mqtt-serial. +Для выполнения запроса необходимо отправить в топик `wb-mqtt-serial/port/Load/client_id`, где client_id - произвольное имя клиента, посылающего запрос, сообщение типа JSON со следующими параметрами: + +#### Параметры + +|Параметр |Тип |Значение по умолчанию |Описание | +|------------------|-------------|----------------------|---------| +|`protocol` |обязательный |`modbus` |протокол обмена с устройством +|`slave_id` |обязательный | |адрес устройства +|`function` |обязательный | |код Modbus-команды +|`address` |обязательный | |адрес первого Modbus-регистра +|`count` |опциональный | 1 |число Modbus-регистров +|`msg` |опциональный | |только данные Modbus-запроса, если необходимо +|`format` |опциональный |`STR` |при указании значения `STR` содержимое параметра `msg` интерпретируется как строка и передается в порт как есть. При указании значения `HEX` содержимое `msg` интерпретируется как шестнадцатеричная строка и перед отправкой в порт преобразуется в массив байт +|`response_timeout`|опциональный |500 ms |таймаут чтения первого байта в миллисекундах +|`frame_timeout` |опциональный |20 ms |таймаут чтения каждого последующего байта в миллисекундах +|`total_timeout` |опциональный |10000ms |таймаут выполнения RPC-запроса в миллисекундах + +##### Обязательные параметры запроса в последовательный порт +|Параметр |Описание | +|-----------|---------| +|`path` |путь к последовательному порту, в который будет отправлено сообщение +|`baud_rate`|скорость порта +|`parity` |чётность - N, O или E +|`data_bits`|количество бит данных +|`stop_bits`|количество стоп-бит + +##### Обязательные параметры запроса в TCP порт +|Параметр |Описание | +|---------|---------| +|`ip` |IP-адрес клиента, которому будет отправлено сообщение +|`port` |номер порта на указанном адресе клиента + +В качестве ответа в топике `wb-mqtt-serial/port/Load/client_id/reply` будет опубликовано сообщение типа JSON со следующими параметрами: + +|Параметр | Описание| +|---------|---------| +|`result`|В случае неуспешного выполнения запроса содержит `null`. В случае успешного выполнения содержит в себе поле `response` или `exception`. Если при запросе в поле `format` было указано `STR`, значение поля `response` представляет собой только данные ответа, и передается запрашивающей стороне как есть. В случае значения `HEX` содержимое поля представляет собой шестнадцатеричную строку. Если устройство вернуло ответ с ошибкой, то передаётся поле `exception`, содержащее структуру из двух полей `code` и `msg`. Где `code` - код Modbus-исключения, `msg` - его текстовая расшифровка. +|`id`| Порядковый номер запроса| +|`error`|В случае успешного выполнения запроса содержит значение `null`. В ином случае содержит параметры, приведенные в таблице ниже + +|Параметр|Описание| +|--------|--------| +|`message`|Строка с кратким описанием ошибки| +|`code`| Код ошибки. Возможные коды ошибок приведены в таблице ниже| +|`data`| Строка с подробным описанием места и условий возникновения ошибки| + +|Код ошибки|Описание| +|----------|--------| +|`-32700`|Ошибка разбора JSON запроса| +|`-32000`|Ошибка выполнения запроса| +|`-32600`|Таймаут выполнения запроса| + + ### Прямая установка параметров связи устройства Существует возможность выполнить прямую установку параметров связи Modbus-устройства Wiren Board с помощью MQTT RPC запроса. Выполнение запроса встраивается в цикл опроса устройств таким образом, что запрос выполнится с высоким приоритетом сразу после окончания текущего цикла опроса. diff --git a/debian/changelog b/debian/changelog index 22dfa036f..537cb9367 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +wb-mqtt-serial (2.141.0) stable; urgency=medium + + * Add Modbus protocol support to port/Load RPC + + -- Petr Krasnoshchekov Wed, 11 Sep 2024 12:38:24 +0500 + wb-mqtt-serial (2.140.0) stable; urgency=medium * Add WB-MAO4-20mA support diff --git a/src/devices/modbus_device.cpp b/src/devices/modbus_device.cpp index 7580499a9..73c6bf80a 100644 --- a/src/devices/modbus_device.cpp +++ b/src/devices/modbus_device.cpp @@ -65,7 +65,6 @@ void TModbusDevice::WriteRegisterImpl(PRegister reg, const TRegisterValue& value Modbus::WriteRegister(*ModbusTraits, *Port(), SlaveId, - 0, *reg, value, ModbusCache, @@ -92,7 +91,6 @@ void TModbusDevice::WriteSetupRegisters() Modbus::WriteSetupRegisters(*ModbusTraits, *Port(), SlaveId, - 0, SetupItems, ModbusCache, DeviceConfig()->RequestDelay, diff --git a/src/devices/modbus_device.h b/src/devices/modbus_device.h index 737a0eee0..684fd437a 100644 --- a/src/devices/modbus_device.h +++ b/src/devices/modbus_device.h @@ -5,6 +5,7 @@ #include #include +#include "serial_config.h" #include "serial_device.h" #include "modbus_common.h" diff --git a/src/devices/modbus_io_device.cpp b/src/devices/modbus_io_device.cpp index 406d91f98..ee425fc51 100644 --- a/src/devices/modbus_io_device.cpp +++ b/src/devices/modbus_io_device.cpp @@ -76,7 +76,6 @@ void TModbusIODevice::WriteRegisterImpl(PRegister reg, const TRegisterValue& val Modbus::WriteRegister(*ModbusTraits, *Port(), SlaveId, - 0, *reg, value, ModbusCache, @@ -102,7 +101,6 @@ void TModbusIODevice::WriteSetupRegisters() Modbus::WriteSetupRegisters(*ModbusTraits, *Port(), SlaveId, - 0, SetupItems, ModbusCache, DeviceConfig()->RequestDelay, diff --git a/src/modbus_base.cpp b/src/modbus_base.cpp new file mode 100644 index 000000000..83d3c2617 --- /dev/null +++ b/src/modbus_base.cpp @@ -0,0 +1,457 @@ +#include "modbus_base.h" + +#include "crc16.h" +#include "serial_exc.h" + +using namespace std; + +namespace +{ + const size_t CRC_SIZE = 2; + + std::string GetModbusExceptionMessage(uint8_t code) + { + if (code == 0) { + return std::string(); + } + // clang-format off + const char* errs[] = + { "illegal function", // 0x01 + "illegal data address", // 0x02 + "illegal data value", // 0x03 + "server device failure", // 0x04 + "long operation (acknowledge)", // 0x05 + "server device is busy", // 0x06 + "", // 0x07 + "memory parity error", // 0x08 + "", // 0x09 + "gateway path is unavailable", // 0x0A + "gateway target device failed to respond" // 0x0B + }; + // clang-format on + --code; + if (code < sizeof(errs) / sizeof(char*)) { + return errs[code]; + } + return "invalid modbus error code (" + std::to_string(code + 1) + ")"; + } + + // throws C++ exception on modbus error code + void ThrowIfModbusException(uint8_t code) + { + if (code == 0) { + return; + } + throw Modbus::TModbusExceptionError(code); + } + + bool IsWriteFunction(Modbus::EFunction function) + { + return function == Modbus::EFunction::FN_WRITE_MULTIPLE_COILS || + function == Modbus::EFunction::FN_WRITE_MULTIPLE_REGISTERS || + function == Modbus::EFunction::FN_WRITE_SINGLE_COIL || + function == Modbus::EFunction::FN_WRITE_SINGLE_REGISTER; + } + + bool IsReadFunction(Modbus::EFunction function) + { + return function == Modbus::EFunction::FN_READ_COILS || function == Modbus::EFunction::FN_READ_DISCRETE || + function == Modbus::EFunction::FN_READ_HOLDING || function == Modbus::EFunction::FN_READ_INPUT; + } + + bool IsSingleBitFunction(Modbus::EFunction function) + { + return function == Modbus::EFunction::FN_READ_COILS || function == Modbus::EFunction::FN_READ_DISCRETE || + function == Modbus::EFunction::FN_WRITE_SINGLE_COIL || + function == Modbus::EFunction::FN_WRITE_MULTIPLE_COILS; + } + + void WriteAs2Bytes(uint8_t* dst, uint16_t val) + { + dst[0] = static_cast(val >> 8); + dst[1] = static_cast(val); + } + + uint16_t GetCoilsByteSize(uint16_t count) + { + return count / 8 + (count % 8 != 0 ? 1 : 0); + } + + std::vector MakeReadRequestPDU(Modbus::EFunction function, uint16_t address, uint16_t count) + { + std::vector res(5); + res[0] = function; + WriteAs2Bytes(res.data() + 1, address); + WriteAs2Bytes(res.data() + 3, count); + return res; + } + + std::vector MakeWriteSingleRequestPDU(Modbus::EFunction function, + uint16_t address, + const std::vector& data) + { + if (data.size() != 2) { + throw Modbus::TMalformedRequestError("data size " + std::to_string(data.size()) + + " doesn't match function code " + std::to_string(function)); + } + std::vector res(5); + res[0] = function; + WriteAs2Bytes(res.data() + 1, address); + res[3] = data[0]; + res[4] = data[1]; + return res; + } + + std::vector MakeWriteMultipleRequestPDU(Modbus::EFunction function, + uint16_t address, + uint16_t count, + const std::vector& data) + { + if ((function == Modbus::EFunction::FN_WRITE_MULTIPLE_COILS && data.size() != GetCoilsByteSize(count)) || + (function == Modbus::EFunction::FN_WRITE_MULTIPLE_REGISTERS && data.size() != count * 2)) + { + throw Modbus::TMalformedRequestError("data size " + std::to_string(data.size()) + + " doesn't match function code " + std::to_string(function)); + } + std::vector res(6 + data.size()); + res[0] = function; + WriteAs2Bytes(res.data() + 1, address); + WriteAs2Bytes(res.data() + 3, count); + res[5] = data.size(); + std::copy(data.begin(), data.end(), res.begin() + 6); + return res; + } + + // get actual function code even if exception + uint8_t GetFunctionCode(uint8_t functionCodeByte) + { + return functionCodeByte & 127; + } + + Modbus::EFunction GetFunction(uint8_t functionCode) + { + if (Modbus::IsSupportedFunction(functionCode)) { + return static_cast(functionCode); + } + throw Modbus::TUnexpectedResponseError("unknown modbus function code: " + to_string(functionCode)); + } +} + +// TModbusRTUTraits + +Modbus::TModbusRTUTraits::TModbusRTUTraits(bool forceFrameTimeout): ForceFrameTimeout(forceFrameTimeout) +{} + +TPort::TFrameCompletePred Modbus::TModbusRTUTraits::ExpectNBytes(size_t n) const +{ + return [=](uint8_t* buf, size_t size) { + if (size < 2) + return false; + if (IsException(buf[1])) // GetPDU + return size >= EXCEPTION_RESPONSE_PDU_SIZE + DATA_SIZE; + return size >= n; + }; +} + +size_t Modbus::TModbusRTUTraits::GetPacketSize(size_t pduSize) const +{ + return DATA_SIZE + pduSize; +} + +void Modbus::TModbusRTUTraits::FinalizeRequest(std::vector& request, uint8_t slaveId) +{ + request[0] = slaveId; + WriteAs2Bytes(&request[request.size() - 2], CRC16::CalculateCRC16(request.data(), request.size() - 2)); +} + +TReadFrameResult Modbus::TModbusRTUTraits::ReadFrame(TPort& port, + uint8_t slaveId, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout, + std::vector& response) const +{ + auto rc = port.ReadFrame(response.data(), + response.size(), + responseTimeout + frameTimeout, + frameTimeout, + ExpectNBytes(response.size())); + // RTU response should be at least 3 bytes: 1 byte slave_id, 2 bytes CRC + if (rc.Count < DATA_SIZE) { + throw Modbus::TMalformedResponseError("invalid data size"); + } + + uint16_t crc = (response[rc.Count - 2] << 8) + response[rc.Count - 1]; + if (crc != CRC16::CalculateCRC16(response.data(), rc.Count - 2)) { + throw Modbus::TMalformedResponseError("invalid crc"); + } + + if (ForceFrameTimeout) { + std::array buf; + try { + port.ReadFrame(buf.data(), buf.size(), frameTimeout, frameTimeout); + } catch (const TResponseTimeoutException& e) { + // No extra data + } + } + + auto responseSlaveId = response[0]; + if (slaveId != responseSlaveId) { + throw Modbus::TUnexpectedResponseError("request and response slave id mismatch"); + } + return rc; +} + +Modbus::TReadResult Modbus::TModbusRTUTraits::Transaction(TPort& port, + uint8_t slaveId, + const std::vector& requestPdu, + size_t expectedResponsePduSize, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout) +{ + std::vector request(GetPacketSize(requestPdu.size())); + std::copy(requestPdu.begin(), requestPdu.end(), request.begin() + 1); + FinalizeRequest(request, slaveId); + + port.WriteBytes(request.data(), request.size()); + + std::vector response(GetPacketSize(expectedResponsePduSize)); + + auto readRes = ReadFrame(port, slaveId, responseTimeout, frameTimeout, response); + + TReadResult res; + res.ResponseTime = readRes.ResponseTime; + res.Pdu.assign(response.begin() + 1, response.begin() + (readRes.Count - CRC_SIZE)); + return res; +} + +// TModbusTCPTraits + +Modbus::TModbusTCPTraits::TModbusTCPTraits(std::shared_ptr transactionId): TransactionId(transactionId) +{} + +void Modbus::TModbusTCPTraits::SetMBAP(std::vector& req, + uint16_t transactionId, + size_t pduSize, + uint8_t slaveId) const +{ + req[0] = ((transactionId >> 8) & 0xFF); + req[1] = (transactionId & 0xFF); + req[2] = 0; // MODBUS + req[3] = 0; + ++pduSize; // length includes additional byte of unit identifier + req[4] = ((pduSize >> 8) & 0xFF); + req[5] = (pduSize & 0xFF); + req[6] = slaveId; +} + +uint16_t Modbus::TModbusTCPTraits::GetLengthFromMBAP(const std::vector& buf) const +{ + uint16_t l = buf[4]; + l <<= 8; + l += buf[5]; + return l; +} + +size_t Modbus::TModbusTCPTraits::GetPacketSize(size_t pduSize) const +{ + return MBAP_SIZE + pduSize; +} + +void Modbus::TModbusTCPTraits::FinalizeRequest(std::vector& request, uint8_t slaveId) +{ + ++(*TransactionId); + SetMBAP(request, *TransactionId, request.size() - MBAP_SIZE, slaveId); +} + +TReadFrameResult Modbus::TModbusTCPTraits::ReadFrame(TPort& port, + uint8_t slaveId, + uint16_t transactionId, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout, + std::vector& response) const +{ + auto startTime = chrono::steady_clock::now(); + while (chrono::duration_cast(chrono::steady_clock::now() - startTime) < + responseTimeout + frameTimeout) + { + if (response.size() < MBAP_SIZE) { + response.resize(MBAP_SIZE); + } + auto rc = port.ReadFrame(response.data(), MBAP_SIZE, responseTimeout + frameTimeout, frameTimeout); + + if (rc.Count < MBAP_SIZE) { + throw Modbus::TMalformedResponseError("Can't read full MBAP"); + } + + auto len = GetLengthFromMBAP(response); + // MBAP length should be at least 1 byte for unit identifier + if (len == 0) { + throw Modbus::TMalformedResponseError("Wrong MBAP length value: 0"); + } + --len; // length includes one byte of unit identifier which is already in buffer + + if (len + MBAP_SIZE > response.size()) { + response.resize(len + MBAP_SIZE); + } + + rc = port.ReadFrame(response.data() + MBAP_SIZE, len, frameTimeout, frameTimeout); + if (rc.Count != len) { + throw Modbus::TMalformedResponseError("Wrong PDU size: " + to_string(rc.Count) + ", expected " + + to_string(len)); + } + rc.Count += MBAP_SIZE; + + // check transaction id + if (((transactionId >> 8) & 0xFF) == response[0] && (transactionId & 0xFF) == response[1]) { + // check unit identifier + if (slaveId != response[6]) { + throw Modbus::TUnexpectedResponseError("request and response unit identifier mismatch"); + } + return rc; + } + } + throw TResponseTimeoutException(); +} + +Modbus::TReadResult Modbus::TModbusTCPTraits::Transaction(TPort& port, + uint8_t slaveId, + const std::vector& requestPdu, + size_t expectedResponsePduSize, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout) +{ + std::vector request(GetPacketSize(requestPdu.size())); + std::copy(requestPdu.begin(), requestPdu.end(), request.begin() + MBAP_SIZE); + FinalizeRequest(request, slaveId); + + port.WriteBytes(request.data(), request.size()); + + std::vector response(GetPacketSize(expectedResponsePduSize)); + + auto readRes = ReadFrame(port, slaveId, *TransactionId, responseTimeout, frameTimeout, response); + + TReadResult res; + res.ResponseTime = readRes.ResponseTime; + res.Pdu.assign(response.begin() + MBAP_SIZE, response.begin() + readRes.Count); + return res; +} + +std::unique_ptr Modbus::TModbusRTUTraitsFactory::GetModbusTraits(PPort port, + bool forceFrameTimeout) +{ + return std::make_unique(forceFrameTimeout); +} + +std::unique_ptr Modbus::TModbusTCPTraitsFactory::GetModbusTraits(PPort port, + bool forceFrameTimeout) +{ + auto it = TransactionIds.find(port); + if (it == TransactionIds.end()) { + std::tie(it, std::ignore) = TransactionIds.insert({port, std::make_shared(0)}); + } + return std::make_unique(it->second); +} + +bool Modbus::IsException(uint8_t functionCode) noexcept +{ + return functionCode & EXCEPTION_BIT; +} + +Modbus::TErrorBase::TErrorBase(const std::string& what): std::runtime_error(what) +{} + +Modbus::TMalformedResponseError::TMalformedResponseError(const std::string& what) + : Modbus::TErrorBase("malformed response: " + what) +{} + +Modbus::TMalformedRequestError::TMalformedRequestError(const std::string& what) + : Modbus::TErrorBase("malformed request: " + what) +{} + +Modbus::TUnexpectedResponseError::TUnexpectedResponseError(const std::string& what): Modbus::TErrorBase(what) +{} + +size_t Modbus::CalcResponsePDUSize(Modbus::EFunction function, size_t registerCount) +{ + if (IsWriteFunction(function)) { + return WRITE_RESPONSE_PDU_SIZE; + } + + if (IsSingleBitFunction(function)) { + return 2 + GetCoilsByteSize(registerCount); + } + return 2 + registerCount * 2; +} + +std::vector Modbus::ExtractResponseData(EFunction requestFunction, const std::vector& pdu) +{ + if (pdu.size() < 2) { + throw Modbus::TMalformedResponseError("PDU is too small"); + } + + auto functionCode = GetFunctionCode(pdu[0]); + if (requestFunction != functionCode) { + throw Modbus::TUnexpectedResponseError("request and response function code mismatch"); + } + + if (IsException(pdu[0])) { + ThrowIfModbusException(pdu[1]); + } + + auto function = GetFunction(functionCode); + if (IsReadFunction(function)) { + size_t byteCount = pdu[1]; + if (pdu.size() != byteCount + 2) { + throw Modbus::TMalformedResponseError("invalid read response byte count: " + std::to_string(byteCount) + + ", got " + std::to_string(pdu.size() - 2)); + } + return std::vector(pdu.begin() + 2, pdu.end()); + } + + if (IsWriteFunction(function)) { + if (WRITE_RESPONSE_PDU_SIZE != pdu.size()) { + throw Modbus::TMalformedResponseError("invalid write response PDU size: " + std::to_string(pdu.size()) + + ", expected " + std::to_string(WRITE_RESPONSE_PDU_SIZE)); + } + } + return std::vector(); +} + +bool Modbus::IsSupportedFunction(uint8_t functionCode) noexcept +{ + auto function = static_cast(functionCode); + return IsReadFunction(function) || IsWriteFunction(function); +} + +std::vector Modbus::MakePDU(Modbus::EFunction function, + uint16_t address, + uint16_t count, + const std::vector& data) +{ + if (IsReadFunction(function)) { + return MakeReadRequestPDU(function, address, count); + } + + if (function == Modbus::EFunction::FN_WRITE_SINGLE_COIL || function == Modbus::EFunction::FN_WRITE_SINGLE_REGISTER) + { + return MakeWriteSingleRequestPDU(function, address, data); + } + + if (function == Modbus::EFunction::FN_WRITE_MULTIPLE_REGISTERS || + function == Modbus::EFunction::FN_WRITE_MULTIPLE_COILS) + { + return MakeWriteMultipleRequestPDU(function, address, count, data); + } + + return std::vector(); +} + +Modbus::TModbusExceptionError::TModbusExceptionError(uint8_t exceptionCode) + : Modbus::TErrorBase(GetModbusExceptionMessage(exceptionCode)), + ExceptionCode(exceptionCode) +{} + +uint8_t Modbus::TModbusExceptionError::GetExceptionCode() const +{ + return ExceptionCode; +} diff --git a/src/modbus_base.h b/src/modbus_base.h new file mode 100644 index 000000000..b053b7966 --- /dev/null +++ b/src/modbus_base.h @@ -0,0 +1,179 @@ +#pragma once + +#include "port.h" + +namespace Modbus +{ + const uint8_t EXCEPTION_BIT = 1 << 7; + const double STANDARD_FRAME_TIMEOUT_BYTES = 3.5; + const size_t EXCEPTION_RESPONSE_PDU_SIZE = 2; + const size_t WRITE_RESPONSE_PDU_SIZE = 5; + + // 1 byte - function code, 2 bytes - starting register address, 2 bytes - quantity of registers + const uint16_t READ_REQUEST_PDU_SIZE = 5; + + // 1 byte - function code, 2 bytes - register address, 2 bytes - value + const uint16_t WRITE_SINGLE_PDU_SIZE = 5; + + // 1 byte - function code, 2 bytes - register address, 2 bytes - quantity of registers, 1 byte- byte count + const uint16_t WRITE_MULTI_PDU_SIZE = 6; + + enum EFunction : uint8_t + { + FN_READ_COILS = 0x1, + FN_READ_DISCRETE = 0x2, + FN_READ_HOLDING = 0x3, + FN_READ_INPUT = 0x4, + FN_WRITE_SINGLE_COIL = 0x5, + FN_WRITE_SINGLE_REGISTER = 0x6, + FN_WRITE_MULTIPLE_COILS = 0xF, + FN_WRITE_MULTIPLE_REGISTERS = 0x10, + }; + + struct TReadResult + { + std::vector Pdu; + + //! Time to first byte + std::chrono::microseconds ResponseTime = std::chrono::microseconds::zero(); + }; + + class IModbusTraits + { + public: + virtual ~IModbusTraits() = default; + + /** + * @brief Read response to specified request. + * + * @throw TSerialDeviceTransientErrorException on timeout. + */ + virtual TReadResult Transaction(TPort& port, + uint8_t slaveId, + const std::vector& requestPdu, + size_t expectedResponsePduSize, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout) = 0; + }; + + class TModbusRTUTraits: public IModbusTraits + { + const size_t DATA_SIZE = 3; // number of bytes in ADU that is not in PDU (slaveID (1b) + crc value (2b)) + + bool ForceFrameTimeout; + + TPort::TFrameCompletePred ExpectNBytes(size_t n) const; + size_t GetPacketSize(size_t pduSize) const; + void FinalizeRequest(std::vector& request, uint8_t slaveId); + TReadFrameResult ReadFrame(TPort& port, + uint8_t slaveId, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout, + std::vector& response) const; + + public: + TModbusRTUTraits(bool forceFrameTimeout = false); + + TReadResult Transaction(TPort& port, + uint8_t slaveId, + const std::vector& requestPdu, + size_t expectedResponsePduSize, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout) override; + }; + + class TModbusTCPTraits: public IModbusTraits + { + const size_t MBAP_SIZE = 7; + + std::shared_ptr TransactionId; + + void SetMBAP(std::vector& req, uint16_t transactionId, size_t pduSize, uint8_t slaveId) const; + uint16_t GetLengthFromMBAP(const std::vector& buf) const; + size_t GetPacketSize(size_t pduSize) const; + void FinalizeRequest(std::vector& request, uint8_t slaveId); + + TReadFrameResult ReadFrame(TPort& port, + uint8_t slaveId, + uint16_t transactionId, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout, + std::vector& response) const; + + public: + TModbusTCPTraits(std::shared_ptr transactionId); + + TReadResult Transaction(TPort& port, + uint8_t slaveId, + const std::vector& requestPdu, + size_t expectedResponsePduSize, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout) override; + }; + + class IModbusTraitsFactory + { + public: + virtual ~IModbusTraitsFactory() = default; + virtual std::unique_ptr GetModbusTraits(PPort port, bool forceFrameTimeout) = 0; + }; + + class TModbusTCPTraitsFactory: public IModbusTraitsFactory + { + std::unordered_map> TransactionIds; + + public: + std::unique_ptr GetModbusTraits(PPort port, bool forceFrameTimeout) override; + }; + + class TModbusRTUTraitsFactory: public IModbusTraitsFactory + { + public: + std::unique_ptr GetModbusTraits(PPort port, bool forceFrameTimeout) override; + }; + + class TErrorBase: public std::runtime_error + { + public: + TErrorBase(const std::string& message); + }; + + class TMalformedResponseError: public TErrorBase + { + public: + TMalformedResponseError(const std::string& what); + }; + + class TMalformedRequestError: public TErrorBase + { + public: + TMalformedRequestError(const std::string& what); + }; + + class TUnexpectedResponseError: public TErrorBase + { + public: + TUnexpectedResponseError(const std::string& what); + }; + + class TModbusExceptionError: public TErrorBase + { + uint8_t ExceptionCode; + + public: + TModbusExceptionError(uint8_t exceptionCode); + + uint8_t GetExceptionCode() const; + }; + + bool IsException(uint8_t functionCode) noexcept; + std::vector ExtractResponseData(EFunction requestFunction, const std::vector& pdu); + size_t CalcResponsePDUSize(Modbus::EFunction function, size_t registerCount); + + bool IsSupportedFunction(uint8_t functionCode) noexcept; + + std::vector MakePDU(Modbus::EFunction function, + uint16_t address, + uint16_t count, + const std::vector& data); +} diff --git a/src/modbus_common.cpp b/src/modbus_common.cpp index 45cdbdf9d..4fbd84b46 100644 --- a/src/modbus_common.cpp +++ b/src/modbus_common.cpp @@ -1,51 +1,24 @@ #include "modbus_common.h" #include "bin_utils.h" -#include "crc16.h" #include "log.h" #include "serial_device.h" -#include -#include -#include -#include -#include -#include -#include - using namespace std; using namespace BinUtils; #define LOG(logger) logger.Log() << "[modbus] " -typedef uint16_t TRegisterWord; - namespace Modbus // modbus protocol declarations { const int MAX_READ_BITS = 2000; const int MAX_READ_REGISTERS = 125; - const size_t WRITE_RESPONSE_PDU_SIZE = 5; - const int MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS = 10; const int MAX_HOLE_CONTINUOUS_1_BIT_REGISTERS = MAX_HOLE_CONTINUOUS_16_BIT_REGISTERS * 8; const uint16_t ENABLE_CONTINUOUS_READ_REGISTER = 114; - enum Error : uint8_t - { - ERR_NONE = 0x0, - ERR_ILLEGAL_FUNCTION = 0x1, - ERR_ILLEGAL_DATA_ADDRESS = 0x2, - ERR_ILLEGAL_DATA_VALUE = 0x3, - ERR_SERVER_DEVICE_FAILURE = 0x4, - ERR_ACKNOWLEDGE = 0x5, - ERR_SERVER_DEVICE_BUSY = 0x6, - ERR_MEMORY_PARITY_ERROR = 0x8, - ERR_GATEWAY_PATH_UNAVAILABLE = 0xA, - ERR_GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0xB - }; - union TAddress { int64_t AbsAddress; @@ -56,19 +29,13 @@ namespace Modbus // modbus protocol declarations }; }; - void ComposeReadRequestPDU(uint8_t* pdu, const TModbusRegisterRange& range, int shift); size_t InferReadResponsePDUSize(int type, size_t registerCount); - // parses modbus response and stores result - void ParseReadResponse(const uint8_t* pdu, - size_t pduSize, + + //! Parses modbus response and stores result + void ParseReadResponse(const std::vector& data, + Modbus::EFunction function, TModbusRegisterRange& range, - Modbus::TRegisterCache& cache); - TReadFrameResult ReadResponse(IModbusTraits& traits, - TPort& port, - const TRequest& request, - TResponse& response, - std::chrono::milliseconds responseTimeout, - std::chrono::milliseconds frameTimeout); + TRegisterCache& cache); } namespace // general utilities @@ -102,16 +69,25 @@ namespace // general utilities { return (type == Modbus::REG_COIL) || (type == Modbus::REG_DISCRETE); } + + void RethrowSerialDeviceException(const Modbus::TModbusExceptionError& err) + { + if (err.GetExceptionCode() == 1 || err.GetExceptionCode() == 2 || err.GetExceptionCode() == 3) { + throw TSerialDevicePermanentRegisterException(err.what()); + } + throw TSerialDeviceTransientErrorException(err.what()); + } } // general utilities namespace Modbus // modbus protocol common utilities { - TMalformedResponseError::TMalformedResponseError(const std::string& what) - : TSerialDeviceTransientErrorException("malformed response: " + what) - {} + enum class OperationType : uint8_t + { + OP_READ = 0, + OP_WRITE + }; - TInvalidCRCError::TInvalidCRCError(): TMalformedResponseError("invalid crc") - {} + EFunction GetFunctionImpl(int registerType, OperationType op, const std::string& typeName, bool many); TModbusRegisterRange::TModbusRegisterRange(std::chrono::microseconds averageResponseTime) : AverageResponseTime(averageResponseTime), @@ -257,23 +233,6 @@ namespace Modbus // modbus protocol common utilities return HasHolesFlg; } - TRequest TModbusRegisterRange::GetRequest(IModbusTraits& traits, uint8_t slaveId, int shift) const - { - TRequest request; - // 1 byte - function code, 2 bytes - starting register address, 2 bytes - quantity of registers - const uint16_t REQUEST_PDU_SIZE = 5; - - request.resize(traits.GetPacketSize(REQUEST_PDU_SIZE)); - Modbus::ComposeReadRequestPDU(traits.GetPDU(request), *this, shift); - traits.FinalizeRequest(request, slaveId, 0); - return request; - } - - size_t TModbusRegisterRange::GetResponseSize(IModbusTraits& traits) const - { - return traits.GetPacketSize(InferReadResponsePDUSize(Type(), GetCount())); - } - const std::string& TModbusRegisterRange::TypeName() const { return RegisterList().front()->TypeName; @@ -312,25 +271,28 @@ namespace Modbus // modbus protocol common utilities if (GetCount() < deviceConfig.MinReadRegisters) { Count = deviceConfig.MinReadRegisters; } - auto request = GetRequest(traits, slaveId, shift); + auto function = GetFunctionImpl(Type(), OperationType::OP_READ, TypeName(), IsPacking(*this)); + auto pdu = Modbus::MakePDU(function, GetStart() + shift, Count, {}); port.SleepSinceLastInteraction(Device()->DeviceConfig()->RequestDelay); - port.WriteBytes(request.data(), request.size()); - TResponse response(GetResponseSize(traits)); - auto readRes = ReadResponse(traits, - port, - request, - response, - Device()->DeviceConfig()->ResponseTimeout, - Device()->DeviceConfig()->FrameTimeout); - ResponseTime = readRes.ResponseTime; - ParseReadResponse(traits.GetPDU(response), readRes.Count, *this, cache); - } catch (const TMalformedResponseError&) { + auto res = traits.Transaction(port, + slaveId, + pdu, + Modbus::CalcResponsePDUSize(function, Count), + Device()->DeviceConfig()->ResponseTimeout, + Device()->DeviceConfig()->FrameTimeout); + ResponseTime = res.ResponseTime; + ParseReadResponse(res.Pdu, function, *this, cache); + } catch (const Modbus::TModbusExceptionError& err) { + RethrowSerialDeviceException(err); + } catch (const Modbus::TMalformedResponseError& err) { try { port.SkipNoise(); } catch (const std::exception& e) { LOG(Warn) << "SkipNoise failed: " << e.what(); } - throw; + throw TSerialDeviceTransientErrorException(err.what()); + } catch (const Modbus::TErrorBase& err) { + throw TSerialDeviceTransientErrorException(err.what()); } } @@ -339,49 +301,29 @@ namespace Modbus // modbus protocol common utilities return ResponseTime; } - ostream& operator<<(ostream& s, const TModbusRegisterRange& range) - { - s << range.GetCount() << " " << range.TypeName() << "(s) @ " << range.GetStart() << " of device " - << range.Device()->ToString(); - return s; - } - - const uint8_t EXCEPTION_BIT = 1 << 7; - - enum ModbusFunction : uint8_t + // returns count of modbus registers needed to represent TModbusRegisterRange + uint16_t TModbusRegisterRange::GetQuantity() const { - FN_READ_COILS = 0x1, - FN_READ_DISCRETE = 0x2, - FN_READ_HOLDING = 0x3, - FN_READ_INPUT = 0x4, - FN_WRITE_SINGLE_COIL = 0x5, - FN_WRITE_SINGLE_REGISTER = 0x6, - FN_WRITE_MULTIPLE_COILS = 0xF, - FN_WRITE_MULTIPLE_REGISTERS = 0x10, - }; + auto type = Type(); - enum class OperationType : uint8_t - { - OP_READ = 0, - OP_WRITE - }; + if (!IsSingleBitType(type) && type != REG_HOLDING && type != REG_HOLDING_SINGLE && type != REG_HOLDING_MULTI && + type != REG_INPUT) + { + throw TSerialDeviceException("invalid register type"); + } - bool IsException(const uint8_t* pdu) - { - return pdu[0] & EXCEPTION_BIT; + return GetCount(); } - // returns modbus exception code if there is any, otherwise 0 - inline uint8_t GetExceptionCode(const uint8_t* pdu) + ostream& operator<<(ostream& s, const TModbusRegisterRange& range) { - if (IsException(pdu)) { - return pdu[1]; // then error code in the next byte - } - return 0; + s << range.GetCount() << " " << range.TypeName() << "(s) @ " << range.GetStart() << " of device " + << range.Device()->ToString(); + return s; } // choose function code for modbus request - uint8_t GetFunctionImpl(int registerType, OperationType op, const std::string& typeName, bool many) + EFunction GetFunctionImpl(int registerType, OperationType op, const std::string& typeName, bool many) { switch (registerType) { case REG_HOLDING_SINGLE: @@ -436,50 +378,11 @@ namespace Modbus // modbus protocol common utilities } } - inline uint8_t GetFunction(const TRegisterConfig& reg, OperationType op) + inline EFunction GetFunction(const TRegisterConfig& reg, OperationType op) { return GetFunctionImpl(reg.Type, op, reg.TypeName, IsPacking(reg)); } - inline uint8_t GetFunction(const TModbusRegisterRange& range, OperationType op) - { - return GetFunctionImpl(range.Type(), op, range.TypeName(), IsPacking(range)); - } - - // throws C++ exception on modbus error code - void ThrowIfModbusException(uint8_t code) - { - if (code == 0) { - return; - } - typedef std::pair TModbusException; - // clang-format off - const TModbusException errs[] = - { {"illegal function", true }, // 0x01 - {"illegal data address", true }, // 0x02 - {"illegal data value", true }, // 0x03 - {"server device failure", false}, // 0x04 - {"long operation (acknowledge)", false}, // 0x05 - {"server device is busy", false}, // 0x06 - {nullptr, false}, // 0x07 - {"memory parity error", false}, // 0x08 - {nullptr, false}, // 0x09 - {"gateway path is unavailable", false}, // 0x0A - {"gateway target device failed to respond", false} }; // 0x0B - // clang-format on - --code; - if (code < sizeof(errs) / sizeof(TModbusException)) { - const auto& err = errs[code]; - if (err.first != nullptr) { - if (err.second) { - throw TSerialDevicePermanentRegisterException(err.first); - } - throw TSerialDeviceTransientErrorException(err.first); - } - } - throw TSerialDeviceTransientErrorException("invalid modbus error code (" + std::to_string(code + 1) + ")"); - } - // returns count of modbus registers needed to represent TRegister uint16_t GetQuantity(const TRegister& reg) { @@ -499,26 +402,6 @@ namespace Modbus // modbus protocol common utilities } } - // returns count of modbus registers needed to represent TModbusRegisterRange - uint16_t GetQuantity(const TModbusRegisterRange& range) - { - auto type = range.Type(); - - if (!IsSingleBitType(type) && type != REG_HOLDING && type != REG_HOLDING_SINGLE && type != REG_HOLDING_MULTI && - type != REG_INPUT) - { - throw TSerialDeviceException("invalid register type"); - } - - return range.GetCount(); - } - - // returns number of bytes needed to hold request - size_t InferWriteRequestPDUSize(const TRegister& reg) - { - return IsPacking(reg) ? 6 + GetModbusDataWidthIn16BitWords(reg) * 2 : 5; - } - // returns number of requests needed to write register size_t InferWriteRequestsCount(const TRegisterConfig& reg) { @@ -535,87 +418,35 @@ namespace Modbus // modbus protocol common utilities } } - inline size_t ReadResponsePDUSize(const uint8_t* pdu) - { - // Modbus stores data byte count in second byte of PDU, - // so PDU size is data size + 2 (1b function code + 1b byte count itself) - return IsException(pdu) ? EXCEPTION_RESPONSE_PDU_SIZE : pdu[1] + 2; - } - - inline size_t WriteResponsePDUSize(const uint8_t* pdu) - { - return IsException(pdu) ? EXCEPTION_RESPONSE_PDU_SIZE : WRITE_RESPONSE_PDU_SIZE; - } - - // fills pdu with read request data according to Modbus specification - void ComposeReadRequestPDU(uint8_t* pdu, const TRegister& reg, int shift) + size_t ComposeStringWriteRequestData(std::vector& data, + const TRegisterConfig& reg, + const std::string& str, + uint16_t baseAddress, + Modbus::TRegisterCache& tmpCache) { - pdu[0] = GetFunction(reg, OperationType::OP_READ); - auto addr = GetUint32RegisterAddress(reg.GetAddress()); - WriteAs2Bytes(pdu + 1, addr + shift); - WriteAs2Bytes(pdu + 3, GetQuantity(reg)); - } - - void ComposeReadRequestPDU(uint8_t* pdu, const TModbusRegisterRange& range, int shift) - { - pdu[0] = GetFunction(range, OperationType::OP_READ); - WriteAs2Bytes(pdu + 1, range.GetStart() + shift); - WriteAs2Bytes(pdu + 3, GetQuantity(range)); - } - - // fills pdu with write request data according to Modbus specification - void ComposeMultipleWriteRequestPDU(uint8_t* pdu, - const TRegisterConfig& reg, - const std::vector& value, - int shift, - Modbus::TRegisterCache& tmpCache, - const Modbus::TRegisterCache& cache) - { - pdu[0] = GetFunction(reg, OperationType::OP_WRITE); - - auto addr = GetUint32RegisterAddress(reg.GetWriteAddress()); - - uint32_t widthInModbusWords = GetModbusDataWidthIn16BitWords(reg); - - auto baseAddress = addr + shift; - + uint32_t regCount = std::min(GetModbusDataWidthIn16BitWords(reg), static_cast(str.size())); + data.resize(regCount * 2); TAddress address{0}; - address.Type = reg.Type; - - WriteAs2Bytes(pdu + 1, baseAddress); - WriteAs2Bytes(pdu + 3, widthInModbusWords); - - pdu[5] = widthInModbusWords * 2; - - for (uint32_t i = 0; (i < widthInModbusWords) && (i < value.size()); ++i) { + for (uint32_t i = 0; (i < regCount); ++i) { address.Address = baseAddress + i; - - auto data = value.at(i); - tmpCache[address.AbsAddress] = data; - WriteAs2Bytes(pdu + 6 + i * 2, data); + uint16_t regData = str[i]; + tmpCache[address.AbsAddress] = regData; + WriteAs2Bytes(data.data() + i * 2, regData); } + return regCount; } - // fills pdu with write request data according to Modbus specification - void ComposeMultipleWriteRequestPDU(uint8_t* pdu, - const TRegisterConfig& reg, - uint64_t value, - int shift, - Modbus::TRegisterCache& tmpCache, - const Modbus::TRegisterCache& cache) + size_t ComposeRawMultipleWriteRequestData(std::vector& data, + const TRegisterConfig& reg, + uint64_t value, + uint16_t baseAddress, + Modbus::TRegisterCache& tmpCache, + const Modbus::TRegisterCache& cache) { - pdu[0] = GetFunction(reg, OperationType::OP_WRITE); - auto addr = GetUint32RegisterAddress(reg.GetWriteAddress()); auto widthInModbusWords = GetModbusDataWidthIn16BitWords(reg); - - auto baseAddress = addr + shift; - - WriteAs2Bytes(pdu + 1, baseAddress); - WriteAs2Bytes(pdu + 3, widthInModbusWords); - - pdu[5] = widthInModbusWords * 2; + data.resize(widthInModbusWords * 2); // Fill value from cache TAddress address{0}; @@ -649,22 +480,23 @@ namespace Modbus // modbus protocol common utilities : valueToWrite & 0xFFFF; tmpCache[address.AbsAddress] = wordValue; - WriteAs2Bytes(pdu + 6 + i * 2, wordValue); + WriteAs2Bytes(data.data() + i * 2, wordValue); if (reg.WordOrder == EWordOrder::BigEndian) { valueToWrite <<= 16; } else { valueToWrite >>= 16; } } + return widthInModbusWords; } - void ComposeSingleWriteRequestPDU(uint8_t* pdu, - const TRegisterConfig& reg, - TRegisterWord value, - int shift, - uint8_t wordIndex, - Modbus::TRegisterCache& tmpCache, - const Modbus::TRegisterCache& cache) + void ComposeRawSingleWriteRequestData(std::vector& data, + const TRegisterConfig& reg, + uint16_t value, + uint16_t baseAddress, + uint8_t wordIndex, + Modbus::TRegisterCache& tmpCache, + const Modbus::TRegisterCache& cache) { auto bitWidth = reg.GetDataWidth(); if (reg.Type == REG_COIL) { @@ -673,11 +505,8 @@ namespace Modbus // modbus protocol common utilities } TAddress address; - address.Type = reg.Type; - - auto addr = GetUint32RegisterAddress(reg.GetWriteAddress()); - address.Address = addr + shift + wordIndex; + address.Address = baseAddress + wordIndex; uint16_t cachedValue = 0; if (cache.count(address.AbsAddress)) { @@ -694,20 +523,16 @@ namespace Modbus // modbus protocol common utilities tmpCache[address.AbsAddress] = wordValue & 0xffff; - pdu[0] = GetFunction(reg, OperationType::OP_WRITE); - - WriteAs2Bytes(pdu + 1, address.Address); - WriteAs2Bytes(pdu + 3, wordValue); + data.resize(2); + WriteAs2Bytes(data.data(), wordValue); } - void ParseSingleBitReadResponse(const uint8_t* pdu, TModbusRegisterRange& range) + void ParseSingleBitReadResponse(const std::vector& data, TModbusRegisterRange& range) { - auto start = pdu + 2; - uint8_t byte_count = pdu[1]; - auto end = start + byte_count; + auto start = data.begin(); auto destination = range.GetBits(); auto coil_count = range.GetCount(); - while (start != end) { + while (start != data.end()) { std::bitset<8> coils(*start++); auto coils_in_byte = std::min(coil_count, 8); for (int i = 0; i < coils_in_byte; ++i) { @@ -766,15 +591,14 @@ namespace Modbus // modbus protocol common utilities return str; } - void FillCache(const uint8_t* pdu, TModbusRegisterRange& range, Modbus::TRegisterCache& cache) + void FillCache(const std::vector& data, TModbusRegisterRange& range, Modbus::TRegisterCache& cache) { TAddress address; address.Type = range.Type(); auto baseAddress = range.GetStart(); - auto start = pdu + 2; - uint8_t byte_count = pdu[1]; + auto start = data.data(); auto data16BitWords = range.GetWords(); - for (int i = 0; i < byte_count / 2; ++i) { + for (size_t i = 0; i < data.size() / 2; ++i) { address.Address = baseAddress + i; cache[address.AbsAddress] = data16BitWords[i] = (*start << 8) | *(start + 1); start += 2; @@ -782,25 +606,18 @@ namespace Modbus // modbus protocol common utilities } // parses modbus response and stores result - void ParseReadResponse(const uint8_t* pdu, - size_t pduSize, + void ParseReadResponse(const std::vector& pdu, + Modbus::EFunction function, TModbusRegisterRange& range, Modbus::TRegisterCache& cache) { - ThrowIfModbusException(GetExceptionCode(pdu)); - - uint8_t byte_count = pdu[1]; - if (pduSize - 2 < byte_count) { - throw TMalformedResponseError("invalid read response byte count: " + std::to_string(byte_count) + ", got " + - std::to_string(pduSize - 2)); - } - + auto data = Modbus::ExtractResponseData(function, pdu); if (IsSingleBitType(range.Type())) { - ParseSingleBitReadResponse(pdu, range); + ParseSingleBitReadResponse(data, range); return; } - FillCache(pdu, range, cache); + FillCache(data, range, cache); auto data16BitWords = range.GetWords(); @@ -814,48 +631,42 @@ namespace Modbus // modbus protocol common utilities } } - // checks modbus response on write - void ParseWriteResponse(const uint8_t* pdu, size_t pduSize) - { - ThrowIfModbusException(GetExceptionCode(pdu)); - auto pdu_size = WriteResponsePDUSize(pdu); - - if (pdu_size != pduSize) { - throw TMalformedResponseError("invalid write response PDU size: " + std::to_string(pdu_size) + - ", expected " + std::to_string(pduSize)); - } - } - PRegisterRange CreateRegisterRange(std::chrono::microseconds averageResponseTime) { return std::make_shared(averageResponseTime); } - TReadFrameResult ReadResponse(IModbusTraits& traits, - TPort& port, - const TRequest& request, - TResponse& response, - std::chrono::milliseconds responseTimeout, - std::chrono::milliseconds frameTimeout) + void WriteTransaction(IModbusTraits& traits, + TPort& port, + uint8_t slaveId, + Modbus::EFunction fn, + size_t responsePduSize, + const std::vector& pdu, + std::chrono::microseconds requestDelay, + std::chrono::milliseconds responseTimeout, + std::chrono::milliseconds frameTimeout) { - auto res = traits.ReadFrame(port, responseTimeout, frameTimeout, request, response); - // PDU size must be at least 2 bytes - if (res.Count < 2) { - throw TMalformedResponseError("Wrong PDU size: " + to_string(res.Count)); - } - auto requestFunctionCode = traits.GetPDU(request)[0]; - auto responseFunctionCode = traits.GetPDU(response)[0] & 127; // get actual function code even if exception - - if (requestFunctionCode != responseFunctionCode) { - throw TSerialDeviceTransientErrorException("request and response function code mismatch"); + try { + port.SleepSinceLastInteraction(requestDelay); + auto res = traits.Transaction(port, slaveId, pdu, responsePduSize, responseTimeout, frameTimeout); + Modbus::ExtractResponseData(fn, res.Pdu); + } catch (const Modbus::TModbusExceptionError& err) { + RethrowSerialDeviceException(err); + } catch (const Modbus::TMalformedResponseError& err) { + try { + port.SkipNoise(); + } catch (const std::exception& e) { + LOG(Warn) << "SkipNoise failed: " << e.what(); + } + throw TSerialDeviceTransientErrorException(err.what()); + } catch (const Modbus::TErrorBase& err) { + throw TSerialDeviceTransientErrorException(err.what()); } - return res; } void WriteRegister(IModbusTraits& traits, TPort& port, uint8_t slaveId, - uint32_t sn, TRegister& reg, const TRegisterValue& value, Modbus::TRegisterCache& cache, @@ -870,60 +681,46 @@ namespace Modbus // modbus protocol common utilities << reg.GetWriteAddress() << " of device " << (reg.Device() ? reg.Device()->ToString() : std::to_string(slaveId)); - // 1 byte - function code, 2 bytes - register address, 2 bytes - value - const uint16_t WRITE_RESPONSE_PDU_SIZE = 5; - TResponse response(traits.GetPacketSize(WRITE_RESPONSE_PDU_SIZE)); - - vector requests(InferWriteRequestsCount(reg)); - + std::vector data; + auto addr = GetUint32RegisterAddress(reg.GetWriteAddress()) + shift; + auto fn = GetFunction(reg, OperationType::OP_WRITE); if (IsPacking(reg)) { - auto& req = requests.front(); - req.resize(traits.GetPacketSize(InferWriteRequestPDUSize(reg))); - - assert(requests.size() == 1 && "only one request is expected when using multiple write"); - // Added workaround for data offset on write - // Strings have their own writing procedure, which does not contain shifts. - if (reg.Format == RegisterFormat::String) { - auto str = value.Get(); - std::vector payloadBuf; - std::for_each(str.begin(), str.end(), [&payloadBuf](char ch) { payloadBuf.push_back(ch); }); - ComposeMultipleWriteRequestPDU(traits.GetPDU(req), reg, payloadBuf, shift, tmpCache, cache); - } else { - ComposeMultipleWriteRequestPDU(traits.GetPDU(req), reg, value.Get(), shift, tmpCache, cache); - } - traits.FinalizeRequest(req, slaveId, sn); + size_t regCount = + reg.Format == RegisterFormat::String + ? ComposeStringWriteRequestData(data, reg, value.Get(), addr, tmpCache) + : ComposeRawMultipleWriteRequestData(data, reg, value.Get(), addr, tmpCache, cache); + WriteTransaction(traits, + port, + slaveId, + fn, + Modbus::CalcResponsePDUSize(fn, regCount), + Modbus::MakePDU(fn, addr, regCount, data), + requestDelay, + responseTimeout, + frameTimeout); } else { + auto requestsCount = InferWriteRequestsCount(reg); auto val = value.Get(); - for (size_t i = 0; i < requests.size(); ++i) { - auto& req = requests[i]; - req.resize(traits.GetPacketSize(InferWriteRequestPDUSize(reg))); - - ComposeSingleWriteRequestPDU(traits.GetPDU(req), - reg, - static_cast(val & 0xffff), - shift, - requests.size() - i - 1, - tmpCache, - cache); - + auto responsePduSize = Modbus::CalcResponsePDUSize(fn, 1); + for (size_t i = 0; i < requestsCount; ++i) { + auto wordIndex = requestsCount - i - 1; + ComposeRawSingleWriteRequestData(data, + reg, + static_cast(val & 0xffff), + addr, + wordIndex, + tmpCache, + cache); + WriteTransaction(traits, + port, + slaveId, + fn, + responsePduSize, + Modbus::MakePDU(fn, addr + wordIndex, 1, data), + requestDelay, + responseTimeout, + frameTimeout); val >>= 16; - traits.FinalizeRequest(req, slaveId, sn); - } - } - - for (const auto& request: requests) { - try { - port.SleepSinceLastInteraction(requestDelay); - port.WriteBytes(request.data(), request.size()); - auto pduSize = ReadResponse(traits, port, request, response, responseTimeout, frameTimeout).Count; - ParseWriteResponse(traits.GetPDU(response), pduSize); - } catch (const TMalformedResponseError&) { - try { - port.SkipNoise(); - } catch (const std::exception& e) { - LOG(Warn) << "SkipNoise failed: " << e.what(); - } - throw; } } @@ -978,7 +775,6 @@ namespace Modbus // modbus protocol common utilities void WriteSetupRegisters(Modbus::IModbusTraits& traits, TPort& port, uint8_t slaveId, - uint32_t sn, const std::vector& setupItems, Modbus::TRegisterCache& cache, std::chrono::microseconds requestDelay, @@ -991,7 +787,6 @@ namespace Modbus // modbus protocol common utilities WriteRegister(traits, port, slaveId, - sn, *item->Register, item->RawValue, cache, @@ -1020,192 +815,6 @@ namespace Modbus // modbus protocol common utilities } } - // TModbusRTUTraits - - TModbusRTUTraits::TModbusRTUTraits(bool forceFrameTimeout): ForceFrameTimeout(forceFrameTimeout) - {} - - TPort::TFrameCompletePred TModbusRTUTraits::ExpectNBytes(size_t n) const - { - return [=](uint8_t* buf, size_t size) { - if (size < 2) - return false; - if (Modbus::IsException(buf + 1)) // GetPDU - return size >= EXCEPTION_RESPONSE_PDU_SIZE + DATA_SIZE; - return size >= n; - }; - } - - size_t TModbusRTUTraits::GetPacketSize(size_t pduSize) const - { - return DATA_SIZE + pduSize; - } - - void TModbusRTUTraits::FinalizeRequest(TRequest& request, uint8_t slaveId, uint32_t sn) - { - request[0] = slaveId; - WriteAs2Bytes(&request[request.size() - 2], CRC16::CalculateCRC16(request.data(), request.size() - 2)); - } - - TReadFrameResult TModbusRTUTraits::ReadFrame(TPort& port, - const std::chrono::milliseconds& responseTimeout, - const std::chrono::milliseconds& frameTimeout, - const TRequest& req, - TResponse& res) const - { - auto rc = port.ReadFrame(res.data(), - res.size(), - responseTimeout + frameTimeout, - frameTimeout, - ExpectNBytes(res.size())); - // RTU response should be at least 3 bytes: 1 byte slave_id, 2 bytes CRC - if (rc.Count < DATA_SIZE) { - throw Modbus::TMalformedResponseError("invalid data size"); - } - - uint16_t crc = (res[rc.Count - 2] << 8) + res[rc.Count - 1]; - if (crc != CRC16::CalculateCRC16(res.data(), rc.Count - 2)) { - throw TInvalidCRCError(); - } - - if (ForceFrameTimeout) { - std::array buf; - try { - port.ReadFrame(buf.data(), buf.size(), frameTimeout, frameTimeout); - } catch (const TResponseTimeoutException& e) { - // No extra data - } - } - - auto requestSlaveId = req[0]; - auto responseSlaveId = res[0]; - if (requestSlaveId != responseSlaveId) { - throw TSerialDeviceTransientErrorException("request and response slave id mismatch"); - } - rc.Count -= DATA_SIZE; - return rc; - } - - uint8_t* TModbusRTUTraits::GetPDU(std::vector& frame) const - { - return &frame[1]; - } - - const uint8_t* TModbusRTUTraits::GetPDU(const std::vector& frame) const - { - return &frame[1]; - } - - // TModbusTCPTraits - - TModbusTCPTraits::TModbusTCPTraits(std::shared_ptr transactionId): TransactionId(transactionId) - {} - - void TModbusTCPTraits::SetMBAP(TRequest& req, uint16_t transactionId, size_t pduSize, uint8_t slaveId) const - { - req[0] = ((transactionId >> 8) & 0xFF); - req[1] = (transactionId & 0xFF); - req[2] = 0; // MODBUS - req[3] = 0; - ++pduSize; // length includes additional byte of unit identifier - req[4] = ((pduSize >> 8) & 0xFF); - req[5] = (pduSize & 0xFF); - req[6] = slaveId; - } - - uint16_t TModbusTCPTraits::GetLengthFromMBAP(const TResponse& buf) const - { - uint16_t l = buf[4]; - l <<= 8; - l += buf[5]; - return l; - } - - size_t TModbusTCPTraits::GetPacketSize(size_t pduSize) const - { - return MBAP_SIZE + pduSize; - } - - void TModbusTCPTraits::FinalizeRequest(TRequest& request, uint8_t slaveId, uint32_t sn) - { - ++(*TransactionId); - SetMBAP(request, *TransactionId, request.size() - MBAP_SIZE, slaveId); - } - - TReadFrameResult TModbusTCPTraits::ReadFrame(TPort& port, - const std::chrono::milliseconds& responseTimeout, - const std::chrono::milliseconds& frameTimeout, - const TRequest& req, - TResponse& res) const - { - auto startTime = chrono::steady_clock::now(); - while (chrono::duration_cast(chrono::steady_clock::now() - startTime) < - responseTimeout + frameTimeout) - { - if (res.size() < MBAP_SIZE) { - res.resize(MBAP_SIZE); - } - auto rc = port.ReadFrame(res.data(), MBAP_SIZE, responseTimeout + frameTimeout, frameTimeout); - - if (rc.Count < MBAP_SIZE) { - throw TMalformedResponseError("Can't read full MBAP"); - } - - auto len = GetLengthFromMBAP(res); - // MBAP length should be at least 1 byte for unit identifier - if (len == 0) { - throw TMalformedResponseError("Wrong MBAP length value: 0"); - } - --len; // length includes one byte of unit identifier which is already in buffer - - if (len + MBAP_SIZE > res.size()) { - res.resize(len + MBAP_SIZE); - } - - rc = port.ReadFrame(res.data() + MBAP_SIZE, len, frameTimeout, frameTimeout); - if (rc.Count != len) { - throw TMalformedResponseError("Wrong PDU size: " + to_string(rc.Count) + ", expected " + - to_string(len)); - } - - // check transaction id - if (req[0] == res[0] && req[1] == res[1]) { - // check unit identifier - if (req[6] != res[6]) { - throw TSerialDeviceTransientErrorException("request and response unit identifier mismatch"); - } - return rc; - } - - LOG(Debug) << "Transaction id mismatch"; - } - throw TResponseTimeoutException(); - } - - uint8_t* TModbusTCPTraits::GetPDU(std::vector& frame) const - { - return &frame[MBAP_SIZE]; - } - - const uint8_t* TModbusTCPTraits::GetPDU(const std::vector& frame) const - { - return &frame[MBAP_SIZE]; - } - - std::unique_ptr TModbusRTUTraitsFactory::GetModbusTraits(PPort port, bool forceFrameTimeout) - { - return std::make_unique(forceFrameTimeout); - } - - std::unique_ptr TModbusTCPTraitsFactory::GetModbusTraits(PPort port, bool forceFrameTimeout) - { - auto it = TransactionIds.find(port); - if (it == TransactionIds.end()) { - std::tie(it, std::ignore) = TransactionIds.insert({port, std::make_shared(0)}); - } - return std::make_unique(it->second); - } - void EnableWbContinuousRead(PSerialDevice device, IModbusTraits& traits, TPort& port, @@ -1218,7 +827,6 @@ namespace Modbus // modbus protocol common utilities Modbus::WriteRegister(traits, port, slaveId, - 0, *reg, TRegisterValue(1), cache, diff --git a/src/modbus_common.h b/src/modbus_common.h index 0b346d735..96a4f3c3f 100644 --- a/src/modbus_common.h +++ b/src/modbus_common.h @@ -1,18 +1,12 @@ #pragma once -#include "port.h" -#include "register.h" -#include "serial_config.h" +#include + +#include "modbus_base.h" #include "serial_device.h" -#include -#include -#include namespace Modbus // modbus protocol common utilities { - - const double STANDARD_FRAME_TIMEOUT_BYTES = 3.5; - enum RegisterType { REG_HOLDING = 0, // used for 'setup' regsb @@ -23,109 +17,8 @@ namespace Modbus // modbus protocol common utilities REG_HOLDING_MULTI, }; - typedef std::vector TRequest; - typedef std::vector TResponse; typedef std::map TRegisterCache; - const size_t EXCEPTION_RESPONSE_PDU_SIZE = 2; - - class IModbusTraits - { - public: - virtual ~IModbusTraits() = default; - - virtual size_t GetPacketSize(size_t pduSize) const = 0; - - virtual void FinalizeRequest(TRequest& request, uint8_t slaveId, uint32_t sn) = 0; - - /** - * @brief Read response to specified request. - * Throws TSerialDeviceTransientErrorException on timeout. - * - * @return size_t PDU size in bytes - */ - virtual TReadFrameResult ReadFrame(TPort& port, - const std::chrono::milliseconds& responseTimeout, - const std::chrono::milliseconds& frameTimeout, - const TRequest& req, - TResponse& resp) const = 0; - - virtual uint8_t* GetPDU(std::vector& frame) const = 0; - virtual const uint8_t* GetPDU(const std::vector& frame) const = 0; - }; - - class TModbusRTUTraits: public IModbusTraits - { - const size_t DATA_SIZE = 3; // number of bytes in ADU that is not in PDU (slaveID (1b) + crc value (2b)) - - bool ForceFrameTimeout; - - TPort::TFrameCompletePred ExpectNBytes(size_t n) const; - - public: - TModbusRTUTraits(bool forceFrameTimeout = false); - - size_t GetPacketSize(size_t pduSize) const override; - - void FinalizeRequest(TRequest& request, uint8_t slaveId, uint32_t sn) override; - - TReadFrameResult ReadFrame(TPort& port, - const std::chrono::milliseconds& responseTimeout, - const std::chrono::milliseconds& frameTimeout, - const TRequest& req, - TResponse& resp) const override; - - uint8_t* GetPDU(std::vector& frame) const override; - const uint8_t* GetPDU(const std::vector& frame) const override; - }; - - class TModbusTCPTraits: public IModbusTraits - { - const size_t MBAP_SIZE = 7; - - std::shared_ptr TransactionId; - - void SetMBAP(TRequest& req, uint16_t transactionId, size_t pduSize, uint8_t slaveId) const; - uint16_t GetLengthFromMBAP(const TResponse& buf) const; - - public: - TModbusTCPTraits(std::shared_ptr transactionId); - - size_t GetPacketSize(size_t pduSize) const override; - - void FinalizeRequest(TRequest& request, uint8_t slaveId, uint32_t sn) override; - - TReadFrameResult ReadFrame(TPort& port, - const std::chrono::milliseconds& responseTimeout, - const std::chrono::milliseconds& frameTimeout, - const TRequest& req, - TResponse& resp) const override; - - uint8_t* GetPDU(std::vector& frame) const override; - const uint8_t* GetPDU(const std::vector& frame) const override; - }; - - class IModbusTraitsFactory - { - public: - virtual ~IModbusTraitsFactory() = default; - virtual std::unique_ptr GetModbusTraits(PPort port, bool forceFrameTimeout) = 0; - }; - - class TModbusTCPTraitsFactory: public IModbusTraitsFactory - { - std::unordered_map> TransactionIds; - - public: - std::unique_ptr GetModbusTraits(PPort port, bool forceFrameTimeout) override; - }; - - class TModbusRTUTraitsFactory: public IModbusTraitsFactory - { - public: - std::unique_ptr GetModbusTraits(PPort port, bool forceFrameTimeout) override; - }; - class TModbusRegisterRange: public TRegisterRange { public: @@ -143,9 +36,6 @@ namespace Modbus // modbus protocol common utilities int Type() const; PSerialDevice Device() const; - TRequest GetRequest(IModbusTraits& traits, uint8_t slaveId, int shift) const; - size_t GetResponseSize(IModbusTraits& traits) const; - void ReadRange(IModbusTraits& traits, TPort& port, uint8_t slaveId, int shift, Modbus::TRegisterCache& cache); std::chrono::microseconds GetResponseTime() const; @@ -160,6 +50,7 @@ namespace Modbus // modbus protocol common utilities std::chrono::microseconds ResponseTime; bool AddingRegisterIncreasesSize(bool isSingleBit, size_t extend) const; + uint16_t GetQuantity() const; }; PRegisterRange CreateRegisterRange(std::chrono::microseconds averageResponseTime); @@ -167,7 +58,6 @@ namespace Modbus // modbus protocol common utilities void WriteRegister(IModbusTraits& traits, TPort& port, uint8_t slaveId, - uint32_t sn, TRegister& reg, const TRegisterValue& value, TRegisterCache& cache, @@ -186,7 +76,6 @@ namespace Modbus // modbus protocol common utilities void WriteSetupRegisters(IModbusTraits& traits, TPort& port, uint8_t slaveId, - uint32_t sn, const std::vector& setupItems, TRegisterCache& cache, std::chrono::microseconds requestDelay, @@ -194,24 +83,9 @@ namespace Modbus // modbus protocol common utilities std::chrono::milliseconds frameTimeout, int shift = 0); - class TMalformedResponseError: public TSerialDeviceTransientErrorException - { - public: - TMalformedResponseError(const std::string& what); - }; - - class TInvalidCRCError: public TMalformedResponseError - { - public: - TInvalidCRCError(); - }; - void EnableWbContinuousRead(PSerialDevice device, IModbusTraits& traits, TPort& port, uint8_t slaveId, TRegisterCache& cache); - - bool IsException(const uint8_t* pdu); - } // modbus protocol common utilities diff --git a/src/modbus_ext_common.cpp b/src/modbus_ext_common.cpp index c631ad56e..396900048 100644 --- a/src/modbus_ext_common.cpp +++ b/src/modbus_ext_common.cpp @@ -3,7 +3,6 @@ #include "bin_utils.h" #include "crc16.h" #include "log.h" -#include "modbus_common.h" #include #include #include @@ -60,6 +59,9 @@ namespace ModbusExt // modbus extension protocol declarations const size_t ENABLE_EVENTS_RESPONSE_DATA_SIZE_POS = 3; const size_t ENABLE_EVENTS_RESPONSE_DATA_POS = 4; + const size_t MODBUS_STANDARD_COMMAND_RESPONSE_SN_POS = 3; + const size_t MODBUS_STANDARD_COMMAND_PDU_POS = 7; + // const size_t ENABLE_EVENTS_REC_TYPE_POS = 0; // const size_t ENABLE_EVENTS_REC_ADDR_POS = 1; // const size_t ENABLE_EVENTS_REC_STATE_POS = 3; @@ -96,11 +98,11 @@ namespace ModbusExt // modbus extension protocol declarations return static_cast(maxBytes); } - Modbus::TRequest MakeReadEventsRequest(const TEventConfirmationState& state, - uint8_t startingSlaveId = 0, - uint8_t maxBytes = EVENTS_REQUEST_MAX_BYTES) + std::vector MakeReadEventsRequest(const TEventConfirmationState& state, + uint8_t startingSlaveId = 0, + uint8_t maxBytes = EVENTS_REQUEST_MAX_BYTES) { - Modbus::TRequest request({BROADCAST_ADDRESS, MODBUS_EXT_COMMAND, EVENTS_REQUEST_COMMAND}); + std::vector request({BROADCAST_ADDRESS, MODBUS_EXT_COMMAND, EVENTS_REQUEST_COMMAND}); auto it = std::back_inserter(request); Append(it, startingSlaveId); Append(it, maxBytes); @@ -118,7 +120,7 @@ namespace ModbusExt // modbus extension protocol declarations auto crc = GetBigEndian(packet + size - CRC_SIZE, packet + size); if (crc != CRC16::CalculateCRC16(packet, size - CRC_SIZE)) { - throw Modbus::TInvalidCRCError(); + throw Modbus::TMalformedResponseError("invalid crc"); } } @@ -149,8 +151,6 @@ namespace ModbusExt // modbus extension protocol declarations try { CheckCRC16(buf, size); - } catch (const Modbus::TInvalidCRCError& err) { - return false; } catch (const Modbus::TMalformedResponseError& err) { return false; } @@ -289,9 +289,8 @@ namespace ModbusExt // modbus extension protocol declarations CheckCRC16(Response.data(), rc); // Old firmwares can send any command with exception bit - if (Response[COMMAND_POS] > 0x80) { - throw TSerialDevicePermanentRegisterException("modbus exception, code " + - std::to_string(Response[EXCEPTION_CODE_POS])); + if (Modbus::IsException(Response[COMMAND_POS])) { + throw Modbus::TModbusExceptionError(Response[EXCEPTION_CODE_POS]); } if (rc < MIN_ENABLE_EVENTS_RESPONSE_SIZE) { @@ -433,7 +432,7 @@ namespace ModbusExt // modbus extension protocol declarations // TModbusTraits - TModbusTraits::TModbusTraits() + TModbusTraits::TModbusTraits(): Sn(0) {} TPort::TFrameCompletePred TModbusTraits::ExpectNBytes(size_t n) const @@ -441,7 +440,7 @@ namespace ModbusExt // modbus extension protocol declarations return [=](uint8_t* buf, size_t size) { if (size < MODBUS_STANDARD_COMMAND_HEADER_SIZE + 1) return false; - if (Modbus::IsException(buf + MODBUS_STANDARD_COMMAND_HEADER_SIZE)) // GetPDU + if (Modbus::IsException(buf[MODBUS_STANDARD_COMMAND_PDU_POS])) // GetPDU return size >= MODBUS_STANDARD_COMMAND_HEADER_SIZE + Modbus::EXCEPTION_RESPONSE_PDU_SIZE + CRC_SIZE; return size >= n; }; @@ -452,15 +451,15 @@ namespace ModbusExt // modbus extension protocol declarations return MODBUS_STANDARD_COMMAND_HEADER_SIZE + CRC_SIZE + pduSize; } - void TModbusTraits::FinalizeRequest(Modbus::TRequest& request, uint8_t slaveId, uint32_t sn) + void TModbusTraits::FinalizeRequest(std::vector& request) { request[0] = BROADCAST_ADDRESS; request[1] = MODBUS_EXT_COMMAND; request[2] = MODBUS_STANDARD_REQUEST_COMMAND; - request[3] = static_cast(sn >> 24); - request[4] = static_cast(sn >> 16); - request[5] = static_cast(sn >> 8); - request[6] = static_cast(sn); + request[3] = static_cast(Sn >> 24); + request[4] = static_cast(Sn >> 16); + request[5] = static_cast(Sn >> 8); + request[6] = static_cast(Sn); auto crc = CRC16::CalculateCRC16(request.data(), request.size() - CRC_SIZE); request[request.size() - 2] = static_cast(crc >> 8); request[request.size() - 1] = static_cast(crc); @@ -469,8 +468,7 @@ namespace ModbusExt // modbus extension protocol declarations TReadFrameResult TModbusTraits::ReadFrame(TPort& port, const milliseconds& responseTimeout, const milliseconds& frameTimeout, - const Modbus::TRequest& req, - Modbus::TResponse& res) const + std::vector& res) const { auto rc = port.ReadFrame(res.data(), res.size(), @@ -483,39 +481,57 @@ namespace ModbusExt // modbus extension protocol declarations } uint16_t crc = (res[rc.Count - 2] << 8) + res[rc.Count - 1]; - if (crc != CRC16::CalculateCRC16(res.data(), rc.Count - 2)) { - throw Modbus::TInvalidCRCError(); + if (crc != CRC16::CalculateCRC16(res.data(), rc.Count - CRC_SIZE)) { + throw Modbus::TMalformedResponseError("invalid crc"); } if (res[0] != BROADCAST_ADDRESS) { - throw TSerialDeviceTransientErrorException("invalid response address"); + throw Modbus::TUnexpectedResponseError("invalid response address"); } if (res[1] != MODBUS_EXT_COMMAND) { - throw TSerialDeviceTransientErrorException("invalid response command"); + throw Modbus::TUnexpectedResponseError("invalid response command"); } if (res[2] != MODBUS_STANDARD_RESPONSE_COMMAND) { - throw TSerialDeviceTransientErrorException("invalid response subcommand"); + throw Modbus::TUnexpectedResponseError("invalid response subcommand"); } - for (size_t i = 3; i < MODBUS_STANDARD_COMMAND_HEADER_SIZE; ++i) { - if (req[i] != res[i]) { - throw TSerialDeviceTransientErrorException("SN mismatch"); - } + auto responseSn = GetBigEndian(res.cbegin() + MODBUS_STANDARD_COMMAND_RESPONSE_SN_POS, + res.cbegin() + MODBUS_STANDARD_COMMAND_HEADER_SIZE); + if (responseSn != Sn) { + throw Modbus::TUnexpectedResponseError("SN mismatch: got " + std::to_string(responseSn) + ", wait " + + std::to_string(Sn)); } - - rc.Count -= MODBUS_STANDARD_COMMAND_HEADER_SIZE + CRC_SIZE; return rc; } - uint8_t* TModbusTraits::GetPDU(std::vector& frame) const + Modbus::TReadResult TModbusTraits::Transaction(TPort& port, + uint8_t slaveId, + const std::vector& requestPdu, + size_t expectedResponsePduSize, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout) { - return &frame[MODBUS_STANDARD_COMMAND_HEADER_SIZE]; + std::vector request(GetPacketSize(requestPdu.size())); + std::copy(requestPdu.begin(), requestPdu.end(), request.begin() + MODBUS_STANDARD_COMMAND_PDU_POS); + FinalizeRequest(request); + + port.WriteBytes(request.data(), request.size()); + + std::vector response(GetPacketSize(expectedResponsePduSize)); + + auto readRes = ReadFrame(port, responseTimeout, frameTimeout, response); + + Modbus::TReadResult res; + res.ResponseTime = readRes.ResponseTime; + res.Pdu.assign(response.begin() + MODBUS_STANDARD_COMMAND_HEADER_SIZE, + response.begin() + (readRes.Count - CRC_SIZE)); + return res; } - const uint8_t* TModbusTraits::GetPDU(const std::vector& frame) const + void TModbusTraits::SetSn(uint32_t sn) { - return &frame[MODBUS_STANDARD_COMMAND_HEADER_SIZE]; + Sn = sn; } } diff --git a/src/modbus_ext_common.h b/src/modbus_ext_common.h index 40b4b1a22..43fde6b36 100644 --- a/src/modbus_ext_common.h +++ b/src/modbus_ext_common.h @@ -1,6 +1,6 @@ #pragma once -#include "modbus_common.h" +#include "modbus_base.h" #include "port.h" namespace ModbusExt // modbus extension protocol common utilities @@ -120,23 +120,27 @@ namespace ModbusExt // modbus extension protocol common utilities class TModbusTraits: public Modbus::IModbusTraits { + uint32_t Sn; TPort::TFrameCompletePred ExpectNBytes(size_t n) const; - public: - TModbusTraits(); - - size_t GetPacketSize(size_t pduSize) const override; - - void FinalizeRequest(Modbus::TRequest& request, uint8_t slaveId, uint32_t sn) override; - + size_t GetPacketSize(size_t pduSize) const; + void FinalizeRequest(std::vector& request); TReadFrameResult ReadFrame(TPort& port, const std::chrono::milliseconds& responseTimeout, const std::chrono::milliseconds& frameTimeout, - const Modbus::TRequest& req, - Modbus::TResponse& resp) const override; + std::vector& response) const; + + public: + TModbusTraits(); + + Modbus::TReadResult Transaction(TPort& port, + uint8_t slaveId, + const std::vector& requestPdu, + size_t expectedResponsePduSize, + const std::chrono::milliseconds& responseTimeout, + const std::chrono::milliseconds& frameTimeout) override; - uint8_t* GetPDU(std::vector& frame) const override; - const uint8_t* GetPDU(const std::vector& frame) const override; + void SetSn(uint32_t sn); }; } // modbus extension protocol common utilities diff --git a/src/rpc/rpc_port_handler.cpp b/src/rpc/rpc_port_handler.cpp index d0a655a3e..cb74876ac 100644 --- a/src/rpc/rpc_port_handler.cpp +++ b/src/rpc/rpc_port_handler.cpp @@ -142,13 +142,19 @@ void TRPCPortHandler::PortLoad(const Json::Value& request, WBMQTT::TMqttRpcServer::TErrorCallback onError) { try { - PRPCPortLoadRequest rpcRequest = ParseRPCPortLoadRequest(request, RequestPortLoadSchema); + WBMQTT::JSON::Validate(request, RequestPortLoadSchema); + } catch (const std::runtime_error& e) { + throw TRPCException(e.what(), TRPCResultCode::RPC_WRONG_PARAM_VALUE); + } + try { PRPCPortDriver rpcPortDriver = FindPortDriver(request); if (rpcPortDriver != nullptr && rpcPortDriver->SerialClient) { - RPCPortLoadHandler(rpcRequest, rpcPortDriver->SerialClient, onResult, onError); + RPCPortLoadHandler(request, rpcPortDriver->SerialClient, onResult, onError); } else { - RPCPortLoadHandler(rpcRequest, InitPort(request), onResult, onError); + auto port = InitPort(request); + port->Open(); + RPCPortLoadHandler(request, *port, onResult, onError); } } catch (const TRPCException& e) { ProcessException(e, onError); @@ -166,7 +172,9 @@ void TRPCPortHandler::PortSetup(const Json::Value& request, if (rpcPortDriver != nullptr && rpcPortDriver->SerialClient) { RPCPortSetupHandler(rpcRequest, rpcPortDriver->SerialClient, onResult, onError); } else { - RPCPortSetupHandler(rpcRequest, InitPort(request), onResult, onError); + auto port = InitPort(request); + port->Open(); + RPCPortSetupHandler(rpcRequest, *port, onResult, onError); } } catch (const TRPCException& e) { ProcessException(e, onError); diff --git a/src/rpc/rpc_port_handler.h b/src/rpc/rpc_port_handler.h index dfe0a9e5b..51bfbb060 100644 --- a/src/rpc/rpc_port_handler.h +++ b/src/rpc/rpc_port_handler.h @@ -6,8 +6,6 @@ #include #include -const std::chrono::seconds DefaultRPCTotalTimeout(10); - // RPC Request execution result code enum class TRPCResultCode { diff --git a/src/rpc/rpc_port_load_handler.cpp b/src/rpc/rpc_port_load_handler.cpp index 030f1df45..a41aafb1f 100644 --- a/src/rpc/rpc_port_load_handler.cpp +++ b/src/rpc/rpc_port_load_handler.cpp @@ -1,150 +1,57 @@ #include "rpc_port_load_handler.h" #include "rpc_port_handler.h" -#include "rpc_port_load_serial_client_task.h" +#include "rpc_port_load_modbus_serial_client_task.h" +#include "rpc_port_load_raw_serial_client_task.h" #include "serial_port.h" #include "tcp_port.h" namespace { - std::vector HexStringToByteVector(const std::string& hexString) + void SetCallbacks(TRPCPortLoadRequest& request, + WBMQTT::TMqttRpcServer::TResultCallback onResult, + WBMQTT::TMqttRpcServer::TErrorCallback onError) { - std::vector byteVector; - if (hexString.size() % 2 != 0) { - throw std::runtime_error("Hex message has odd char count"); - } - for (size_t i = 0; i < hexString.size(); i += 2) { - auto byte = strtol(hexString.substr(i, 2).c_str(), NULL, 16); - byteVector.push_back(byte); - } - - return byteVector; - } - - std::string ByteVectorToHexString(const std::vector& byteVector) - { - std::stringstream ss; - ss << std::hex << std::setfill('0'); - - for (size_t i = 0; i < byteVector.size(); i++) { - ss << std::hex << std::setw(2) << static_cast(byteVector[i]); - } - - return ss.str(); - } - - std::string PortLoadResponseFormat(const std::vector& response, TRPCMessageFormat format) - { - std::string responseStr; - if (format == TRPCMessageFormat::RPC_MESSAGE_FORMAT_HEX) { - responseStr = ByteVectorToHexString(response); - } else { - responseStr.assign(response.begin(), response.end()); - } - - return responseStr; - } - - std::vector SendRequest(PPort port, PRPCPortLoadRequest rpcRequest) - { - port->Open(); - port->WriteBytes(rpcRequest->Message); - - std::vector response(rpcRequest->ResponseSize); - auto actualSize = port->ReadFrame(response.data(), - rpcRequest->ResponseSize, - rpcRequest->ResponseTimeout, - rpcRequest->FrameTimeout) - .Count; - response.resize(actualSize); - - return response; + request.OnResult = onResult; + request.OnError = onError; } } // namespace -TSerialPortConnectionSettings ParseRPCSerialPortSettings(const Json::Value& request) -{ - TSerialPortConnectionSettings res; - WBMQTT::JSON::Get(request, "baud_rate", res.BaudRate); - if (request.isMember("parity")) { - res.Parity = request["parity"].asCString()[0]; - } - WBMQTT::JSON::Get(request, "data_bits", res.DataBits); - WBMQTT::JSON::Get(request, "stop_bits", res.StopBits); - return res; -} - -PRPCPortLoadRequest ParseRPCPortLoadRequest(const Json::Value& request, const Json::Value& requestSchema) -{ - PRPCPortLoadRequest RPCRequest = std::make_shared(); - - try { - WBMQTT::JSON::Validate(request, requestSchema); - - std::string messageStr, formatStr; - WBMQTT::JSON::Get(request, "response_size", RPCRequest->ResponseSize); - WBMQTT::JSON::Get(request, "format", formatStr); - WBMQTT::JSON::Get(request, "msg", messageStr); - - if (formatStr == "HEX") { - RPCRequest->Format = TRPCMessageFormat::RPC_MESSAGE_FORMAT_HEX; - } else { - RPCRequest->Format = TRPCMessageFormat::RPC_MESSAGE_FORMAT_STR; - } - - if (RPCRequest->Format == TRPCMessageFormat::RPC_MESSAGE_FORMAT_HEX) { - RPCRequest->Message = HexStringToByteVector(messageStr); - } else { - RPCRequest->Message.assign(messageStr.begin(), messageStr.end()); - } - - if (!WBMQTT::JSON::Get(request, "response_timeout", RPCRequest->ResponseTimeout)) { - RPCRequest->ResponseTimeout = DefaultResponseTimeout; - } - - if (!WBMQTT::JSON::Get(request, "frame_timeout", RPCRequest->FrameTimeout)) { - RPCRequest->FrameTimeout = DefaultFrameTimeout; - } - - if (!WBMQTT::JSON::Get(request, "total_timeout", RPCRequest->TotalTimeout)) { - RPCRequest->TotalTimeout = DefaultRPCTotalTimeout; - } - - RPCRequest->SerialPortSettings = ParseRPCSerialPortSettings(request); - - } catch (const std::runtime_error& e) { - throw TRPCException(e.what(), TRPCResultCode::RPC_WRONG_PARAM_VALUE); - } - - return RPCRequest; -} - -void RPCPortLoadHandler(PRPCPortLoadRequest rpcRequest, +void RPCPortLoadHandler(const Json::Value& request, PSerialClient serialClient, WBMQTT::TMqttRpcServer::TResultCallback onResult, WBMQTT::TMqttRpcServer::TErrorCallback onError) { - rpcRequest->OnResult = [onResult, rpcRequest](const std::vector& response) { - Json::Value replyJSON; - replyJSON["response"] = PortLoadResponseFormat(response, rpcRequest->Format); - onResult(replyJSON); - }; - rpcRequest->OnError = onError; - if (serialClient) { - auto task(std::make_shared(rpcRequest)); - serialClient->AddTask(task); - } else { + if (!serialClient) { throw TRPCException("SerialClient wasn't found for requested port", TRPCResultCode::RPC_WRONG_PORT); } + auto protocol = request.get("protocol", "raw").asString(); + if (protocol == "modbus") { + auto rpcRequest = ParseRPCPortLoadModbusRequest(request); + SetCallbacks(*rpcRequest, onResult, onError); + serialClient->AddTask(std::make_shared(rpcRequest)); + return; + } + auto rpcRequest = ParseRPCPortLoadRawRequest(request); + SetCallbacks(*rpcRequest, onResult, onError); + serialClient->AddTask(std::make_shared(rpcRequest)); } -void RPCPortLoadHandler(PRPCPortLoadRequest rpcRequest, - PPort port, +void RPCPortLoadHandler(const Json::Value& request, + TPort& port, WBMQTT::TMqttRpcServer::TResultCallback onResult, WBMQTT::TMqttRpcServer::TErrorCallback onError) { Json::Value replyJSON; - auto response = SendRequest(port, rpcRequest); - replyJSON["response"] = PortLoadResponseFormat(response, rpcRequest->Format); + auto protocol = request.get("protocol", "raw").asString(); + if (protocol == "modbus") { + auto rpcRequest = ParseRPCPortLoadModbusRequest(request); + auto response = ExecRPCPortLoadModbusRequest(port, rpcRequest); + replyJSON["response"] = FormatResponse(response, rpcRequest->Format); + } else { + auto rpcRequest = ParseRPCPortLoadRawRequest(request); + auto response = ExecRPCPortLoadRawRequest(port, rpcRequest); + replyJSON["response"] = FormatResponse(response, rpcRequest->Format); + } onResult(replyJSON); } diff --git a/src/rpc/rpc_port_load_handler.h b/src/rpc/rpc_port_load_handler.h index 884628ce3..9fe00dbf5 100644 --- a/src/rpc/rpc_port_load_handler.h +++ b/src/rpc/rpc_port_load_handler.h @@ -1,18 +1,15 @@ #pragma once -#include "rpc_port_load_request.h" -#include "serial_client.h" - -TSerialPortConnectionSettings ParseRPCSerialPortSettings(const Json::Value& request); +#include -PRPCPortLoadRequest ParseRPCPortLoadRequest(const Json::Value& request, const Json::Value& requestSchema); +#include "serial_client.h" -void RPCPortLoadHandler(PRPCPortLoadRequest rpcRequest, +void RPCPortLoadHandler(const Json::Value& request, PSerialClient serialClient, WBMQTT::TMqttRpcServer::TResultCallback onResult, WBMQTT::TMqttRpcServer::TErrorCallback onError); -void RPCPortLoadHandler(PRPCPortLoadRequest rpcRequest, - PPort port, +void RPCPortLoadHandler(const Json::Value& request, + TPort& port, WBMQTT::TMqttRpcServer::TResultCallback onResult, WBMQTT::TMqttRpcServer::TErrorCallback onError); diff --git a/src/rpc/rpc_port_load_modbus_serial_client_task.cpp b/src/rpc/rpc_port_load_modbus_serial_client_task.cpp new file mode 100644 index 000000000..935829476 --- /dev/null +++ b/src/rpc/rpc_port_load_modbus_serial_client_task.cpp @@ -0,0 +1,116 @@ +#include "rpc_port_load_modbus_serial_client_task.h" +#include "modbus_base.h" +#include "rpc_port_handler.h" +#include "rpc_port_load_handler.h" +#include "serial_exc.h" +#include "serial_port.h" + +template<> bool inline WBMQTT::JSON::Is(const Json::Value& value) +{ + return value.isUInt(); +} + +template<> inline uint8_t WBMQTT::JSON::As(const Json::Value& value) +{ + return value.asUInt() & 0xFF; +} + +template<> bool inline WBMQTT::JSON::Is(const Json::Value& value) +{ + return value.isUInt(); +} + +template<> inline uint16_t WBMQTT::JSON::As(const Json::Value& value) +{ + return value.asUInt() & 0xFFFF; +} + +template<> bool inline WBMQTT::JSON::Is(const Json::Value& value) +{ + if (!value.isUInt()) { + return false; + } + auto fun = value.asUInt(); + return Modbus::IsSupportedFunction(fun & 0xFF); +} + +template<> inline Modbus::EFunction WBMQTT::JSON::As(const Json::Value& value) +{ + return static_cast(value.asUInt() & 0xFF); +} + +PRPCPortLoadModbusRequest ParseRPCPortLoadModbusRequest(const Json::Value& request) +{ + PRPCPortLoadModbusRequest RPCRequest = std::make_shared(); + + try { + ParseRPCPortLoadRequest(request, *RPCRequest); + WBMQTT::JSON::Get(request, "slave_id", RPCRequest->SlaveId); + WBMQTT::JSON::Get(request, "address", RPCRequest->Address); + WBMQTT::JSON::Get(request, "count", RPCRequest->Count); + WBMQTT::JSON::Get(request, "function", RPCRequest->Function); + } catch (const std::runtime_error& e) { + throw TRPCException(e.what(), TRPCResultCode::RPC_WRONG_PARAM_VALUE); + } + + return RPCRequest; +} + +std::vector ExecRPCPortLoadModbusRequest(TPort& port, PRPCPortLoadModbusRequest rpcRequest) +{ + Modbus::TModbusRTUTraits traits; + auto pdu = Modbus::MakePDU(rpcRequest->Function, rpcRequest->Address, rpcRequest->Count, rpcRequest->Message); + auto responsePduSize = Modbus::CalcResponsePDUSize(rpcRequest->Function, rpcRequest->Count); + auto res = traits.Transaction(port, + rpcRequest->SlaveId, + pdu, + responsePduSize, + rpcRequest->ResponseTimeout, + rpcRequest->FrameTimeout); + return Modbus::ExtractResponseData(rpcRequest->Function, res.Pdu); +} + +TRPCPortLoadModbusSerialClientTask::TRPCPortLoadModbusSerialClientTask(PRPCPortLoadModbusRequest request) + : Request(request) +{ + Request = request; + ExpireTime = std::chrono::steady_clock::now() + Request->TotalTimeout; +} + +ISerialClientTask::TRunResult TRPCPortLoadModbusSerialClientTask::Run( + PPort port, + TSerialClientDeviceAccessHandler& lastAccessedDevice) +{ + if (std::chrono::steady_clock::now() > ExpireTime) { + if (Request->OnError) { + Request->OnError(WBMQTT::E_RPC_REQUEST_TIMEOUT, "RPC request timeout"); + } + return ISerialClientTask::TRunResult::OK; + } + + try { + port->CheckPortOpen(); + port->SkipNoise(); + port->SleepSinceLastInteraction(Request->FrameTimeout); + lastAccessedDevice.PrepareToAccess(nullptr); + + TSerialPortSettingsGuard settingsGuard(port, Request->SerialPortSettings); + auto response = ExecRPCPortLoadModbusRequest(*port, Request); + + if (Request->OnResult) { + Json::Value replyJSON; + replyJSON["response"] = FormatResponse(response, Request->Format); + Request->OnResult(replyJSON); + } + } catch (const Modbus::TModbusExceptionError& error) { + Json::Value replyJSON; + replyJSON["exception"]["code"] = error.GetExceptionCode(); + replyJSON["exception"]["msg"] = error.what(); + Request->OnResult(replyJSON); + } catch (const std::exception& error) { + if (Request->OnError) { + Request->OnError(WBMQTT::E_RPC_SERVER_ERROR, std::string("Port IO error: ") + error.what()); + } + } + return ISerialClientTask::TRunResult::OK; +} diff --git a/src/rpc/rpc_port_load_modbus_serial_client_task.h b/src/rpc/rpc_port_load_modbus_serial_client_task.h new file mode 100644 index 000000000..33c44b1b3 --- /dev/null +++ b/src/rpc/rpc_port_load_modbus_serial_client_task.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "rpc_port_load_request.h" +#include "serial_client.h" + +class TRPCPortLoadModbusRequest: public TRPCPortLoadRequest +{ +public: + uint8_t SlaveId; + uint16_t Address; + size_t Count; + Modbus::EFunction Function; +}; + +typedef std::shared_ptr PRPCPortLoadModbusRequest; + +PRPCPortLoadModbusRequest ParseRPCPortLoadModbusRequest(const Json::Value& request); + +class TRPCPortLoadModbusSerialClientTask: public ISerialClientTask +{ +public: + TRPCPortLoadModbusSerialClientTask(PRPCPortLoadModbusRequest request); + + ISerialClientTask::TRunResult Run(PPort port, TSerialClientDeviceAccessHandler& lastAccessedDevice) override; + +private: + PRPCPortLoadModbusRequest Request; + std::chrono::steady_clock::time_point ExpireTime; +}; + +typedef std::shared_ptr PRPCPortLoadModbusSerialClientTask; + +std::vector ExecRPCPortLoadModbusRequest(TPort& port, PRPCPortLoadModbusRequest rpcRequest); diff --git a/src/rpc/rpc_port_load_raw_serial_client_task.cpp b/src/rpc/rpc_port_load_raw_serial_client_task.cpp new file mode 100644 index 000000000..0410f0849 --- /dev/null +++ b/src/rpc/rpc_port_load_raw_serial_client_task.cpp @@ -0,0 +1,70 @@ +#include "rpc_port_load_raw_serial_client_task.h" +#include "rpc_port_handler.h" +#include "rpc_port_load_handler.h" +#include "serial_exc.h" +#include "serial_port.h" + +PRPCPortLoadRawRequest ParseRPCPortLoadRawRequest(const Json::Value& request) +{ + PRPCPortLoadRawRequest RPCRequest = std::make_shared(); + + try { + ParseRPCPortLoadRequest(request, *RPCRequest); + WBMQTT::JSON::Get(request, "response_size", RPCRequest->ResponseSize); + } catch (const std::runtime_error& e) { + throw TRPCException(e.what(), TRPCResultCode::RPC_WRONG_PARAM_VALUE); + } + + return RPCRequest; +} + +std::vector ExecRPCPortLoadRawRequest(TPort& port, PRPCPortLoadRawRequest rpcRequest) +{ + port.WriteBytes(rpcRequest->Message); + + std::vector response(rpcRequest->ResponseSize); + auto actualSize = + port.ReadFrame(response.data(), rpcRequest->ResponseSize, rpcRequest->ResponseTimeout, rpcRequest->FrameTimeout) + .Count; + response.resize(actualSize); + + return response; +} + +TRPCPortLoadRawSerialClientTask::TRPCPortLoadRawSerialClientTask(PRPCPortLoadRawRequest request): Request(request) +{ + Request = request; + ExpireTime = std::chrono::steady_clock::now() + Request->TotalTimeout; +} + +ISerialClientTask::TRunResult TRPCPortLoadRawSerialClientTask::Run(PPort port, + TSerialClientDeviceAccessHandler& lastAccessedDevice) +{ + if (std::chrono::steady_clock::now() > ExpireTime) { + if (Request->OnError) { + Request->OnError(WBMQTT::E_RPC_REQUEST_TIMEOUT, "RPC request timeout"); + } + return ISerialClientTask::TRunResult::OK; + } + + try { + port->CheckPortOpen(); + port->SkipNoise(); + port->SleepSinceLastInteraction(Request->FrameTimeout); + lastAccessedDevice.PrepareToAccess(nullptr); + + TSerialPortSettingsGuard settingsGuard(port, Request->SerialPortSettings); + auto response = ExecRPCPortLoadRawRequest(*port, Request); + + if (Request->OnResult) { + Json::Value replyJSON; + replyJSON["response"] = FormatResponse(response, Request->Format); + Request->OnResult(replyJSON); + } + } catch (const std::exception& error) { + if (Request->OnError) { + Request->OnError(WBMQTT::E_RPC_SERVER_ERROR, std::string("Port IO error: ") + error.what()); + } + } + return ISerialClientTask::TRunResult::OK; +} diff --git a/src/rpc/rpc_port_load_raw_serial_client_task.h b/src/rpc/rpc_port_load_raw_serial_client_task.h new file mode 100644 index 000000000..cd7870ee9 --- /dev/null +++ b/src/rpc/rpc_port_load_raw_serial_client_task.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include "rpc_port_load_request.h" +#include "serial_client.h" + +class TRPCPortLoadRawRequest: public TRPCPortLoadRequest +{ +public: + size_t ResponseSize; +}; + +typedef std::shared_ptr PRPCPortLoadRawRequest; + +PRPCPortLoadRawRequest ParseRPCPortLoadRawRequest(const Json::Value& request); + +class TRPCPortLoadRawSerialClientTask: public ISerialClientTask +{ +public: + TRPCPortLoadRawSerialClientTask(PRPCPortLoadRawRequest request); + + ISerialClientTask::TRunResult Run(PPort port, TSerialClientDeviceAccessHandler& lastAccessedDevice) override; + +private: + PRPCPortLoadRawRequest Request; + std::chrono::steady_clock::time_point ExpireTime; +}; + +typedef std::shared_ptr PRPCPortLoadRawSerialClientTask; + +std::vector ExecRPCPortLoadRawRequest(TPort& port, PRPCPortLoadRawRequest rpcRequest); diff --git a/src/rpc/rpc_port_load_request.cpp b/src/rpc/rpc_port_load_request.cpp new file mode 100644 index 000000000..6ad536827 --- /dev/null +++ b/src/rpc/rpc_port_load_request.cpp @@ -0,0 +1,81 @@ +#include "rpc_port_load_request.h" + +namespace +{ + std::string ByteVectorToHexString(const std::vector& byteVector) + { + std::stringstream ss; + ss << std::hex << std::setfill('0'); + + for (const auto& byte: byteVector) { + ss << std::hex << std::setw(2) << static_cast(byte); + } + + return ss.str(); + } +} + +std::vector HexStringToByteVector(const std::string& hexString) +{ + std::vector byteVector; + if (hexString.size() % 2 != 0) { + throw std::runtime_error("Hex message has odd char count"); + } + + for (size_t i = 0; i < hexString.size(); i += 2) { + auto byte = strtol(hexString.substr(i, 2).c_str(), NULL, 16); + byteVector.push_back(byte); + } + + return byteVector; +} + +void ParseRPCPortLoadRequest(const Json::Value& data, TRPCPortLoadRequest& request) +{ + + std::string messageStr, formatStr; + WBMQTT::JSON::Get(data, "format", formatStr); + WBMQTT::JSON::Get(data, "msg", messageStr); + + if (formatStr == "HEX") { + request.Format = TRPCMessageFormat::RPC_MESSAGE_FORMAT_HEX; + } else { + request.Format = TRPCMessageFormat::RPC_MESSAGE_FORMAT_STR; + } + + if (request.Format == TRPCMessageFormat::RPC_MESSAGE_FORMAT_HEX) { + request.Message = HexStringToByteVector(messageStr); + } else { + request.Message.assign(messageStr.begin(), messageStr.end()); + } + + WBMQTT::JSON::Get(data, "response_timeout", request.ResponseTimeout); + WBMQTT::JSON::Get(data, "frame_timeout", request.FrameTimeout); + WBMQTT::JSON::Get(data, "total_timeout", request.TotalTimeout); + + request.SerialPortSettings = ParseRPCSerialPortSettings(data); +} + +TSerialPortConnectionSettings ParseRPCSerialPortSettings(const Json::Value& request) +{ + TSerialPortConnectionSettings res; + WBMQTT::JSON::Get(request, "baud_rate", res.BaudRate); + if (request.isMember("parity")) { + res.Parity = request["parity"].asCString()[0]; + } + WBMQTT::JSON::Get(request, "data_bits", res.DataBits); + WBMQTT::JSON::Get(request, "stop_bits", res.StopBits); + return res; +} + +std::string FormatResponse(const std::vector& response, TRPCMessageFormat format) +{ + std::string responseStr; + if (format == TRPCMessageFormat::RPC_MESSAGE_FORMAT_HEX) { + responseStr = ByteVectorToHexString(response); + } else { + responseStr.assign(response.begin(), response.end()); + } + + return responseStr; +} \ No newline at end of file diff --git a/src/rpc/rpc_port_load_request.h b/src/rpc/rpc_port_load_request.h index f6dff3681..4b1ff4c2a 100644 --- a/src/rpc/rpc_port_load_request.h +++ b/src/rpc/rpc_port_load_request.h @@ -3,8 +3,11 @@ #include #include +#include "serial_device.h" #include "serial_port_settings.h" +const std::chrono::seconds DefaultRPCTotalTimeout(10); + enum class TRPCMessageFormat { RPC_MESSAGE_FORMAT_HEX, @@ -15,16 +18,20 @@ class TRPCPortLoadRequest { public: std::vector Message; - std::chrono::milliseconds ResponseTimeout; - std::chrono::milliseconds FrameTimeout; - std::chrono::milliseconds TotalTimeout; + std::chrono::milliseconds ResponseTimeout = DefaultResponseTimeout; + std::chrono::milliseconds FrameTimeout = DefaultFrameTimeout; + std::chrono::milliseconds TotalTimeout = DefaultRPCTotalTimeout; TRPCMessageFormat Format; - size_t ResponseSize; TSerialPortConnectionSettings SerialPortSettings; - std::function&)> OnResult = nullptr; + WBMQTT::TMqttRpcServer::TResultCallback OnResult = nullptr; WBMQTT::TMqttRpcServer::TErrorCallback OnError = nullptr; }; typedef std::shared_ptr PRPCPortLoadRequest; + +TSerialPortConnectionSettings ParseRPCSerialPortSettings(const Json::Value& request); +void ParseRPCPortLoadRequest(const Json::Value& data, TRPCPortLoadRequest& request); +std::string FormatResponse(const std::vector& response, TRPCMessageFormat format); +std::vector HexStringToByteVector(const std::string& hexString); diff --git a/src/rpc/rpc_port_load_serial_client_task.cpp b/src/rpc/rpc_port_load_serial_client_task.cpp deleted file mode 100644 index 43bb5cc97..000000000 --- a/src/rpc/rpc_port_load_serial_client_task.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "rpc_port_load_serial_client_task.h" -#include "serial_exc.h" -#include "serial_port.h" - -TRPCPortLoadSerialClientTask::TRPCPortLoadSerialClientTask(PRPCPortLoadRequest request): Request(request) -{ - Request = request; - ExpireTime = std::chrono::steady_clock::now() + Request->TotalTimeout; -} - -ISerialClientTask::TRunResult TRPCPortLoadSerialClientTask::Run(PPort port, - TSerialClientDeviceAccessHandler& lastAccessedDevice) -{ - if (std::chrono::steady_clock::now() > ExpireTime) { - if (Request->OnError) { - Request->OnError(WBMQTT::E_RPC_REQUEST_TIMEOUT, "RPC request timeout"); - } - return ISerialClientTask::TRunResult::OK; - } - - try { - port->CheckPortOpen(); - port->SkipNoise(); - port->SleepSinceLastInteraction(Request->FrameTimeout); - lastAccessedDevice.PrepareToAccess(nullptr); - - TSerialPortSettingsGuard settingsGuard(port, Request->SerialPortSettings); - - port->WriteBytes(Request->Message); - - std::vector response(Request->ResponseSize); - size_t actualSize = - port->ReadFrame(response.data(), Request->ResponseSize, Request->ResponseTimeout, Request->FrameTimeout) - .Count; - - response.resize(actualSize); - if (Request->OnResult) { - Request->OnResult(response); - } - } catch (const TSerialDeviceException& error) { - if (Request->OnError) { - Request->OnError(WBMQTT::E_RPC_SERVER_ERROR, std::string("Port IO error: ") + error.what()); - } - } - return ISerialClientTask::TRunResult::OK; -} diff --git a/src/rpc/rpc_port_load_serial_client_task.h b/src/rpc/rpc_port_load_serial_client_task.h deleted file mode 100644 index 80251c90c..000000000 --- a/src/rpc/rpc_port_load_serial_client_task.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "rpc_port_load_request.h" -#include "serial_client.h" -#include -#include - -class TRPCPortLoadSerialClientTask: public ISerialClientTask -{ -public: - TRPCPortLoadSerialClientTask(PRPCPortLoadRequest request); - - ISerialClientTask::TRunResult Run(PPort port, TSerialClientDeviceAccessHandler& lastAccessedDevice) override; - -private: - PRPCPortLoadRequest Request; - std::chrono::steady_clock::time_point ExpireTime; -}; - -typedef std::shared_ptr PRPCPortLoadSerialClientTask; diff --git a/src/rpc/rpc_port_setup_handler.cpp b/src/rpc/rpc_port_setup_handler.cpp index ec822cff5..297ee2b02 100644 --- a/src/rpc/rpc_port_setup_handler.cpp +++ b/src/rpc/rpc_port_setup_handler.cpp @@ -99,24 +99,25 @@ void RPCPortSetupHandler(PRPCPortSetupRequest rpcRequest, } void RPCPortSetupHandler(PRPCPortSetupRequest rpcRequest, - PPort port, + TPort& port, WBMQTT::TMqttRpcServer::TResultCallback onResult, WBMQTT::TMqttRpcServer::TErrorCallback onError) { - Modbus::TModbusRTUTraitsFactory traitsFactory; - auto rtuTraits = traitsFactory.GetModbusTraits(port, false); ModbusExt::TModbusTraits fastModbusTraits; - Modbus::TRegisterCache cache; + Modbus::TModbusRTUTraits rtuTraits(false); auto frameTimeout = - std::chrono::ceil(port->GetSendTimeBytes(Modbus::STANDARD_FRAME_TIMEOUT_BYTES)); - port->Open(); + std::chrono::ceil(port.GetSendTimeBytes(Modbus::STANDARD_FRAME_TIMEOUT_BYTES)); for (auto item: rpcRequest->Items) { - TSerialPortSettingsGuard settingsGuard(port, item.SerialPortSettings); - port->SleepSinceLastInteraction(frameTimeout); - Modbus::WriteSetupRegisters(item.Sn ? fastModbusTraits : *rtuTraits, - *port, + port.ApplySerialPortSettings(item.SerialPortSettings); + port.SleepSinceLastInteraction(frameTimeout); + if (item.Sn) { + fastModbusTraits.SetSn(item.Sn.value()); + } + Modbus::TRegisterCache cache; + Modbus::WriteSetupRegisters(item.Sn ? static_cast(fastModbusTraits) + : static_cast(rtuTraits), + port, item.SlaveId, - item.Sn.value_or(0), item.Regs, cache, std::chrono::microseconds(0), diff --git a/src/rpc/rpc_port_setup_handler.h b/src/rpc/rpc_port_setup_handler.h index a0135bf38..07d62cc84 100644 --- a/src/rpc/rpc_port_setup_handler.h +++ b/src/rpc/rpc_port_setup_handler.h @@ -11,6 +11,6 @@ void RPCPortSetupHandler(PRPCPortSetupRequest rpcRequest, WBMQTT::TMqttRpcServer::TErrorCallback onError); void RPCPortSetupHandler(PRPCPortSetupRequest rpcRequest, - PPort port, + TPort& port, WBMQTT::TMqttRpcServer::TResultCallback onResult, WBMQTT::TMqttRpcServer::TErrorCallback onError); diff --git a/src/rpc/rpc_port_setup_serial_client_task.cpp b/src/rpc/rpc_port_setup_serial_client_task.cpp index 213722647..556003e33 100644 --- a/src/rpc/rpc_port_setup_serial_client_task.cpp +++ b/src/rpc/rpc_port_setup_serial_client_task.cpp @@ -22,21 +22,23 @@ ISerialClientTask::TRunResult TRPCPortSetupSerialClientTask::Run(PPort port, try { port->CheckPortOpen(); port->SkipNoise(); - Modbus::TModbusRTUTraitsFactory traitsFactory; - auto rtuTraits = traitsFactory.GetModbusTraits(port, false); ModbusExt::TModbusTraits fastModbusTraits; + Modbus::TModbusRTUTraits rtuTraits(false); + auto frameTimeout = + std::chrono::ceil(port->GetSendTimeBytes(Modbus::STANDARD_FRAME_TIMEOUT_BYTES)); for (auto item: Request->Items) { TSerialPortSettingsGuard settingsGuard(port, item.SerialPortSettings); - auto frameTimeout = std::chrono::ceil( - port->GetSendTimeBytes(Modbus::STANDARD_FRAME_TIMEOUT_BYTES)); port->SleepSinceLastInteraction(frameTimeout); lastAccessedDevice.PrepareToAccess(nullptr); + if (item.Sn) { + fastModbusTraits.SetSn(item.Sn.value()); + } Modbus::TRegisterCache cache; - Modbus::WriteSetupRegisters(item.Sn ? fastModbusTraits : *rtuTraits, + Modbus::WriteSetupRegisters(item.Sn ? static_cast(fastModbusTraits) + : static_cast(rtuTraits), *port, item.SlaveId, - item.Sn.value_or(0), item.Regs, cache, std::chrono::microseconds(0), @@ -47,7 +49,7 @@ ISerialClientTask::TRunResult TRPCPortSetupSerialClientTask::Run(PPort port, if (Request->OnResult) { Request->OnResult(); } - } catch (const TSerialDeviceException& error) { + } catch (const std::exception& error) { if (Request->OnError) { Request->OnError(WBMQTT::E_RPC_SERVER_ERROR, std::string("Port IO error: ") + error.what()); } diff --git a/src/serial_client_events_reader.cpp b/src/serial_client_events_reader.cpp index 4d1ea7f9e..46f29f27b 100644 --- a/src/serial_client_events_reader.cpp +++ b/src/serial_client_events_reader.cpp @@ -104,6 +104,8 @@ namespace } try { enabler.SendRequests(); + } catch (const Modbus::TErrorBase& ex) { + LOG(Warn) << "Failed to disable unexpected events: " << ex.what(); } catch (const TSerialDeviceException& ex) { LOG(Warn) << "Failed to disable unexpected events: " << ex.what(); } @@ -210,6 +212,17 @@ TSerialClientEventsReader::TSerialClientEventsReader(size_t maxReadErrors) ClearErrorsOnSuccessfulRead(false) {} +void TSerialClientEventsReader::ReadEventsFailed(const std::string& errorMessage, TRegisterCallback registerCallback) +{ + LOG(Warn) << "Reading events failed: " << errorMessage; + ++ReadErrors; + if (ReadErrors > MaxReadErrors) { + SetReadErrors(registerCallback); + ReadErrors = 0; + ClearErrorsOnSuccessfulRead = true; + } +} + void TSerialClientEventsReader::ReadEvents(TPort& port, milliseconds maxReadingTime, TRegisterCallback registerCallback, @@ -236,13 +249,9 @@ void TSerialClientEventsReader::ReadEvents(TPort& port, LastAccessedSlaveId = visitor.GetSlaveId(); ClearReadErrors(registerCallback); } catch (const TSerialDeviceException& ex) { - LOG(Warn) << "Reading events failed: " << ex.what(); - ++ReadErrors; - if (ReadErrors > MaxReadErrors) { - SetReadErrors(registerCallback); - ReadErrors = 0; - ClearErrorsOnSuccessfulRead = true; - } + ReadEventsFailed(ex.what(), registerCallback); + } catch (const Modbus::TErrorBase& ex) { + ReadEventsFailed(ex.what(), registerCallback); } } DisableEventsFromRegs(port, visitor.GetRegsToDisable()); @@ -280,11 +289,11 @@ void TSerialClientEventsReader::EnableEvents(PSerialDevice device, TPort& port) LOG(Debug) << "Try to enable events for " << MakeDeviceDescriptionString(slaveId); ev.SendRequests(); } - } catch (const TSerialDevicePermanentRegisterException& e) { + } catch (const Modbus::TModbusExceptionError& e) { LOG(Warn) << "Failed to enable events for " << MakeDeviceDescriptionString(slaveId) << ": " << e.what(); } catch (const TResponseTimeoutException& e) { LOG(Warn) << "Failed to enable events for " << MakeDeviceDescriptionString(slaveId) << ": " << e.what(); - } catch (const TSerialDeviceTransientErrorException& e) { + } catch (const Modbus::TErrorBase& e) { throw TSerialDeviceTransientErrorException(std::string("Failed to enable events: ") + e.what()); } } diff --git a/src/serial_client_events_reader.h b/src/serial_client_events_reader.h index d26098890..8d2cb090d 100644 --- a/src/serial_client_events_reader.h +++ b/src/serial_client_events_reader.h @@ -66,4 +66,5 @@ class TSerialClientEventsReader void OnEnabledEvent(uint8_t slaveId, uint8_t type, uint16_t addr, bool res); void ClearReadErrors(TRegisterCallback callback); + void ReadEventsFailed(const std::string& errorMessage, TRegisterCallback registerCallback); }; diff --git a/test/modbus_ext_common_test.cpp b/test/modbus_ext_common_test.cpp index dd42ae548..2ee579b1e 100644 --- a/test/modbus_ext_common_test.cpp +++ b/test/modbus_ext_common_test.cpp @@ -144,7 +144,7 @@ TEST(TModbusExtTest, EventsEnablerIllegalFunction) ModbusExt::TEventsEnabler ev(10, port, [](uint8_t, uint16_t, bool) {}); ev.AddRegister(101, ModbusExt::TEventType::COIL, ModbusExt::TEventPriority::HIGH); - EXPECT_THROW(ev.SendRequests(), TSerialDevicePermanentRegisterException); + EXPECT_THROW(ev.SendRequests(), Modbus::TModbusExceptionError); } TEST(TModbusExtTest, EventsEnablerTwoRanges) @@ -427,47 +427,20 @@ class TModbusExtTraitsTest: public testing::Test } }; -TEST_F(TModbusExtTraitsTest, PacketSize) -{ - ModbusExt::TModbusTraits traits; - ASSERT_EQ(traits.GetPacketSize(10), 19); -} - -TEST_F(TModbusExtTraitsTest, GetPDU) -{ - ModbusExt::TModbusTraits traits; - - const Modbus::TRequest r = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; - const Modbus::TRequest r2 = {110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124}; - - ASSERT_EQ(*traits.GetPDU(r), 7); - ASSERT_EQ(*traits.GetPDU(r2), 117); -} - -TEST_F(TModbusExtTraitsTest, FinalizeRequest) -{ - ModbusExt::TModbusTraits traits; - - Modbus::TRequest r = {0, 1, 2, 3, 4, 5, 6, 0x06, 0x00, 0x80, 0x00, 0x02, 12, 13}; - Modbus::TRequest p = {0xfd, 0x46, 0x08, 0xfe, 0xca, 0xe7, 0xe5, 0x06, 0x00, 0x80, 0x00, 0x02, 0x4d, 0x2a}; - traits.FinalizeRequest(r, 100, 0xfecae7e5); - - TestEqual(r, p); -} - TEST_F(TModbusExtTraitsTest, ReadFrameGood) { TPortMock port; port.Response = {0xfd, 0x46, 0x09, 0xfe, 0xca, 0xe7, 0xe5, 0x06, 0x00, 0x80, 0x00, 0x02, 0x1c, 0xef}; ModbusExt::TModbusTraits traits; + traits.SetSn(0xfecae7e5); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0xfd, 0x46, 0x08, 0xfe, 0xca, 0xe7, 0xe5, 0x06, 0x00, 0x80, 0x00, 0x02, 0x4d, 0x2a}; - Modbus::TRequest resp(port.Response.size()); + std::vector req = {0x06, 0x00, 0x80, 0x00, 0x02}; - ASSERT_EQ(traits.ReadFrame(port, t, t, req, resp).Count, 5); + auto resp = traits.Transaction(port, 0, req, req.size(), t, t); + ASSERT_EQ(resp.Pdu.size(), req.size()); - TestEqual(resp, port.Response); + TestEqual(resp.Pdu, req); } TEST_F(TModbusExtTraitsTest, ReadFrameTooSmallError) @@ -475,12 +448,12 @@ TEST_F(TModbusExtTraitsTest, ReadFrameTooSmallError) TPortMock port; port.Response = {0xfd, 0x46, 0x09, 0xfe, 0xca}; ModbusExt::TModbusTraits traits; + traits.SetSn(0xfecae7e5); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0xfd, 0x46, 0x08, 0xfe, 0xca, 0xe7, 0xe5, 0x06, 0x00, 0x80, 0x00, 0x02, 0x4d, 0x2a}; - Modbus::TRequest resp(port.Response.size()); + std::vector req = {0x06, 0x00, 0x80, 0x00, 0x02}; - ASSERT_THROW(traits.ReadFrame(port, t, t, req, resp), Modbus::TMalformedResponseError); + ASSERT_THROW(traits.Transaction(port, 0, req, req.size(), t, t), Modbus::TMalformedResponseError); } TEST_F(TModbusExtTraitsTest, ReadFrameInvalidCrc) @@ -488,12 +461,12 @@ TEST_F(TModbusExtTraitsTest, ReadFrameInvalidCrc) TPortMock port; port.Response = {0xfd, 0x46, 0x09, 0xfe, 0xca, 0xe7, 0xe5, 0x06, 0x00, 0x80, 0x00, 0x02, 0x10, 0xef}; ModbusExt::TModbusTraits traits; + traits.SetSn(0xfecae7e5); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0xfd, 0x46, 0x08, 0xfe, 0xca, 0xe7, 0xe5, 0x06, 0x00, 0x80, 0x00, 0x02, 0x4d, 0x2a}; - Modbus::TRequest resp(port.Response.size()); + std::vector req = {0x06, 0x00, 0x80, 0x00, 0x02}; - ASSERT_THROW(traits.ReadFrame(port, t, t, req, resp), Modbus::TInvalidCRCError); + ASSERT_THROW(traits.Transaction(port, 0, req, req.size(), t, t), Modbus::TMalformedResponseError); } TEST_F(TModbusExtTraitsTest, ReadFrameInvalidHeader) @@ -503,21 +476,21 @@ TEST_F(TModbusExtTraitsTest, ReadFrameInvalidHeader) SetCrc(port.Response); ModbusExt::TModbusTraits traits; + traits.SetSn(0xfecae7e5); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0xfd, 0x46, 0x08, 0xfe, 0xca, 0xe7, 0xe5, 0x06, 0x00, 0x80, 0x00, 0x02, 0x4d, 0x2a}; - Modbus::TRequest resp(port.Response.size()); + std::vector req = {0x06, 0x00, 0x80, 0x00, 0x02}; ASSERT_THROW( { try { - traits.ReadFrame(port, t, t, req, resp); - } catch (const TSerialDeviceTransientErrorException& e) { - EXPECT_STREQ("Serial protocol error: invalid response address", e.what()); + traits.Transaction(port, 0, req, req.size(), t, t); + } catch (const Modbus::TUnexpectedResponseError& e) { + EXPECT_STREQ("invalid response address", e.what()); throw; } }, - TSerialDeviceTransientErrorException); + Modbus::TUnexpectedResponseError); port.Response[0] = 0xfd; port.Response[1] = 0x45; @@ -525,13 +498,13 @@ TEST_F(TModbusExtTraitsTest, ReadFrameInvalidHeader) ASSERT_THROW( { try { - traits.ReadFrame(port, t, t, req, resp); - } catch (const TSerialDeviceTransientErrorException& e) { - EXPECT_STREQ("Serial protocol error: invalid response command", e.what()); + traits.Transaction(port, 0, req, req.size(), t, t); + } catch (const Modbus::TUnexpectedResponseError& e) { + EXPECT_STREQ("invalid response command", e.what()); throw; } }, - TSerialDeviceTransientErrorException); + Modbus::TUnexpectedResponseError); port.Response[1] = 0x46; port.Response[2] = 0x10; @@ -539,13 +512,13 @@ TEST_F(TModbusExtTraitsTest, ReadFrameInvalidHeader) ASSERT_THROW( { try { - traits.ReadFrame(port, t, t, req, resp); - } catch (const TSerialDeviceTransientErrorException& e) { - EXPECT_STREQ("Serial protocol error: invalid response subcommand", e.what()); + traits.Transaction(port, 0, req, req.size(), t, t); + } catch (const Modbus::TUnexpectedResponseError& e) { + EXPECT_STREQ("invalid response subcommand", e.what()); throw; } }, - TSerialDeviceTransientErrorException); + Modbus::TUnexpectedResponseError); port.Response[2] = 0x09; port.Response[3] = 0x01; @@ -553,11 +526,11 @@ TEST_F(TModbusExtTraitsTest, ReadFrameInvalidHeader) ASSERT_THROW( { try { - traits.ReadFrame(port, t, t, req, resp); - } catch (const TSerialDeviceTransientErrorException& e) { - EXPECT_STREQ("Serial protocol error: SN mismatch", e.what()); + traits.Transaction(port, 0, req, req.size(), t, t); + } catch (const Modbus::TUnexpectedResponseError& e) { + EXPECT_STREQ("SN mismatch: got 30074853, wait 4274710501", e.what()); throw; } }, - TSerialDeviceTransientErrorException); + Modbus::TUnexpectedResponseError); } diff --git a/test/modbus_tcp_test.cpp b/test/modbus_tcp_test.cpp index 640fd2e93..e7036c778 100644 --- a/test/modbus_tcp_test.cpp +++ b/test/modbus_tcp_test.cpp @@ -74,52 +74,17 @@ class TModbusTCPTraitsTest: public testing::Test } }; -TEST_F(TModbusTCPTraitsTest, PacketSize) -{ - Modbus::TModbusTCPTraits traits(std::make_shared(10)); - ASSERT_EQ(traits.GetPacketSize(10), 17); // Packet size == PDU size + MBAP size (7 bytes) -} - -TEST_F(TModbusTCPTraitsTest, GetPDU) -{ - Modbus::TModbusTCPTraits traits(std::make_shared(10)); - - Modbus::TRequest r = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - const Modbus::TRequest r2 = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19}; - - ASSERT_EQ(*traits.GetPDU(r), 7); - ASSERT_EQ(*traits.GetPDU(r2), 17); -} - -TEST_F(TModbusTCPTraitsTest, FinalizeRequest) -{ - Modbus::TModbusTCPTraits traits(std::make_shared(10)); - - Modbus::TRequest r = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - Modbus::TRequest p = {0, 11, 0, 0, 0, 4, 100, 7, 8, 9}; - traits.FinalizeRequest(r, 100, 0); - - Modbus::TRequest r2 = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; - Modbus::TRequest p2 = {0, 12, 0, 0, 0, 5, 200, 17, 18, 19, 20}; - traits.FinalizeRequest(r2, 200, 0); - - TestEqual(r, p); - TestEqual(r2, p2); -} - TEST_F(TModbusTCPTraitsTest, ReadFrameGood) { - std::vector r = {0, 1, 0, 0, 0, 2, 100, 17}; + std::vector r = {0, 1, 0, 0, 0, 3, 100, 17, 18}; TPortMock port(r); - Modbus::TModbusTCPTraits traits(std::make_shared(10)); + Modbus::TModbusTCPTraits traits(std::make_shared(0)); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0, 1, 0, 0, 0, 4, 100, 7, 8, 9}; - Modbus::TRequest resp; - - ASSERT_EQ(traits.ReadFrame(port, t, t, req, resp).Count, 1); - - TestEqual(resp, r); + std::vector req = {7, 8, 9}; + auto resp = traits.Transaction(port, 100, req, 2, t, t).Pdu; + ASSERT_EQ(resp.size(), 2); + TestEqual(resp, {17, 18}); } TEST_F(TModbusTCPTraitsTest, ReadFrameSmallMBAP) @@ -128,10 +93,8 @@ TEST_F(TModbusTCPTraitsTest, ReadFrameSmallMBAP) Modbus::TModbusTCPTraits traits(std::make_shared(10)); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0, 1, 0, 0, 0, 4, 100, 7, 8, 9}; - Modbus::TRequest resp; - - ASSERT_THROW(traits.ReadFrame(port, t, t, req, resp), Modbus::TMalformedResponseError); + std::vector req = {7, 8, 9}; + ASSERT_THROW(traits.Transaction(port, 100, req, 2, t, t), Modbus::TMalformedResponseError); } TEST_F(TModbusTCPTraitsTest, ReadFrameSmallMBAPLength) @@ -140,10 +103,8 @@ TEST_F(TModbusTCPTraitsTest, ReadFrameSmallMBAPLength) Modbus::TModbusTCPTraits traits(std::make_shared(10)); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0, 1, 0, 0, 0, 4, 100, 7, 8, 9}; - Modbus::TRequest resp; - - ASSERT_THROW(traits.ReadFrame(port, t, t, req, resp), Modbus::TMalformedResponseError); + std::vector req = {7, 8, 9}; + ASSERT_THROW(traits.Transaction(port, 100, req, 2, t, t), Modbus::TMalformedResponseError); } TEST_F(TModbusTCPTraitsTest, ReadFrameSmallPDU) @@ -152,40 +113,32 @@ TEST_F(TModbusTCPTraitsTest, ReadFrameSmallPDU) Modbus::TModbusTCPTraits traits(std::make_shared(10)); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0, 1, 0, 0, 0, 4, 100, 7, 8, 9}; - Modbus::TRequest resp; - - ASSERT_THROW(traits.ReadFrame(port, t, t, req, resp), Modbus::TMalformedResponseError); + std::vector req = {7, 8, 9}; + ASSERT_THROW(traits.Transaction(port, 100, req, 2, t, t), Modbus::TMalformedResponseError); } TEST_F(TModbusTCPTraitsTest, ReadFrameWrongUnitId) { TPortMock port({0, 1, 0, 0, 0, 4, 101, 7, 8, 9}); - Modbus::TModbusTCPTraits traits(std::make_shared(10)); + Modbus::TModbusTCPTraits traits(std::make_shared(0)); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0, 1, 0, 0, 0, 4, 100, 7, 8, 9}; - Modbus::TRequest resp; - - ASSERT_THROW(traits.ReadFrame(port, t, t, req, resp), TSerialDeviceTransientErrorException); + std::vector req = {7, 8, 9}; + ASSERT_THROW(traits.Transaction(port, 100, req, 2, t, t), Modbus::TUnexpectedResponseError); } TEST_F(TModbusTCPTraitsTest, ReadFramePassWrongTransactionId) { - Modbus::TResponse goodResp = {0, 1, 0, 0, 0, 4, 100, 17, 18, 19}; + std::vector goodResp = {0, 1, 0, 0, 0, 4, 100, 17, 18, 19}; std::vector r = {0, 2, 0, 0, 0, 8, 101, 7, 8, 9, 10, 11, 12, 13}; r.insert(r.end(), goodResp.begin(), goodResp.end()); TPortMock port(r); - Modbus::TModbusTCPTraits traits(std::make_shared(10)); + Modbus::TModbusTCPTraits traits(std::make_shared(0)); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0, 1, 0, 0, 0, 4, 100, 7, 8, 9}; - Modbus::TRequest resp; - - auto pduSize = traits.ReadFrame(port, t, t, req, resp).Count; - resp.resize(traits.GetPacketSize(pduSize)); - TestEqual(resp, goodResp); + std::vector req = {7, 8, 9}; + TestEqual(traits.Transaction(port, 100, req, 2, t, t).Pdu, {17, 18, 19}); } TEST_F(TModbusTCPTraitsTest, ReadFrameTimeout) @@ -194,8 +147,6 @@ TEST_F(TModbusTCPTraitsTest, ReadFrameTimeout) Modbus::TModbusTCPTraits traits(std::make_shared(10)); std::chrono::milliseconds t(10); - Modbus::TRequest req = {0, 1, 0, 0, 0, 4, 100, 7, 8, 9}; - Modbus::TRequest resp; - - ASSERT_THROW(traits.ReadFrame(port, t, t, req, resp), TSerialDeviceTransientErrorException); + std::vector req = {7, 8, 9}; + ASSERT_THROW(traits.Transaction(port, 100, req, 2, t, t), TResponseTimeoutException); } diff --git a/test/serial_client_test.cpp b/test/serial_client_test.cpp index 78efcae8f..26158dc63 100644 --- a/test/serial_client_test.cpp +++ b/test/serial_client_test.cpp @@ -2,7 +2,7 @@ #include "fake_serial_port.h" #include "log.h" #include "rpc/rpc_port_handler.h" -#include "rpc/rpc_port_load_serial_client_task.h" +#include "rpc/rpc_port_load_raw_serial_client_task.h" #include "serial_driver.h" #include @@ -1493,14 +1493,15 @@ TRPCResultCode TSerialClientIntegrationTest::SendRPCRequest(PMQTTSerialDriver se TRPCResultCode resultCode = TRPCResultCode::RPC_OK; std::vector responseInt; - PRPCPortLoadRequest request = std::make_shared(); + PRPCPortLoadRawRequest request = std::make_shared(); request->ResponseTimeout = std::chrono::milliseconds(500); request->FrameTimeout = std::chrono::milliseconds(20); request->TotalTimeout = totalTimeout; std::copy(expectedRequest.begin(), expectedRequest.end(), back_inserter(request->Message)); request->ResponseSize = expectedResponseLength; - request->OnResult = [&responseInt](const std::vector& response) { - std::copy(response.begin(), response.end(), back_inserter(responseInt)); + request->OnResult = [&responseInt](const Json::Value& response) { + auto str = HexStringToByteVector(response["response"].asString()); + std::copy(str.begin(), str.end(), back_inserter(responseInt)); }; request->OnError = [&resultCode](const TMqttRpcErrorCode code, const std::string&) { resultCode = @@ -1509,7 +1510,7 @@ TRPCResultCode TSerialClientIntegrationTest::SendRPCRequest(PMQTTSerialDriver se try { Note() << "Send RPC request"; - PRPCPortLoadSerialClientTask task(std::make_shared(request)); + PRPCPortLoadRawSerialClientTask task(std::make_shared(request)); serialClient->AddTask(task); SerialDriver->LoopOnce(); EXPECT_EQ(responseInt == expectedResponse, true); diff --git a/wb-mqtt-serial-rpc-port-load-request.schema.json b/wb-mqtt-serial-rpc-port-load-request.schema.json index b37164a4a..5587c5b94 100644 --- a/wb-mqtt-serial-rpc-port-load-request.schema.json +++ b/wb-mqtt-serial-rpc-port-load-request.schema.json @@ -1,47 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "RPC request schema", "type": "object", - "properties": { - "msg": { - "description": "Message to send via RPC", - "type": "string" - }, - "response_size": { - "description": "Expected response size", - "type": "integer", - "minimum": 0 - }, - "response_timeout": { - "description": "Timeout in milliseconds for response first byte receiving, default 500 (DefaultResponseTimeout from wb-mqtt-serial)", - "type": "integer", - "minimum": 0 - }, - "frame_timeout": { - "description": "Timeout in milliseconds between bytes receiving in port io, default 20 (DefaultFrameTimeout from wb-mqtt-serial)", - "type": "integer", - "minimum": 0 - }, - "total_timeout": { - "description": "Request execution time in seconds including queue time and thread operations, default 10", - "type": "integer", - "minimum": 0 - }, - "format": { - "description": "If format is HEX, msg interprets as string with only hex digits", - "type": "string", - "enum": [ - "HEX", - "STR" - ] - } - }, - "required": [ - "msg", - "response_size" - ], - "oneOf": [ - { + "definitions": { + "serial_port_config": { "properties": { "path": { "description": "Path to serial port", @@ -76,7 +37,7 @@ "stop_bits" ] }, - { + "tcp_port_config": { "properties": { "ip": { "description": "Client ip address", @@ -94,6 +55,88 @@ "ip", "port" ] + }, + "timeouts_config": { + "properties": { + "response_timeout": { + "type": "integer", + "minimum": 0 + }, + "frame_timeout": { + "type": "integer", + "minimum": 0 + }, + "total_timeout": { + "type": "integer", + "minimum": 0 + } + } + }, + "request_data": { + "properties": { + "msg": { + "type": "string" + }, + "format": { + "type": "string", + "enum": [ "HEX", "STR" ] + } + } + }, + "raw_request": { + "properties": { + "protocol": { + "type": "string", + "enum": [ "raw" ] + }, + "response_size": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ "msg", "response_size" ] + }, + "modbus_request": { + "properties": { + "protocol": { + "type": "string", + "enum": [ "modbus" ] + }, + "slave_id": { + "type": "integer", + "minimum": 0 + }, + "function": { + "type": "integer", + "enum": [1, 2, 3, 4, 6, 15, 16] + }, + "address": { + "type": "integer", + "minimum": 0 + }, + "count": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ "protocol", "slave_id", "function", "address" ] + } + }, + + "allOf": [ + { + "oneOf": [ + { "$ref" : "#/definitions/serial_port_config"}, + { "$ref" : "#/definitions/tcp_port_config"} + ] + }, + { "$ref" : "#/definitions/timeouts_config" }, + { "$ref" : "#/definitions/request_data" }, + { + "oneOf": [ + { "$ref" : "#/definitions/raw_request"}, + { "$ref" : "#/definitions/modbus_request"} + ] } ] -} \ No newline at end of file +}