Files
templates/c/set-protocol/tests/test_modbus_server.c
T

131 lines
11 KiB
C

/*
* Проверки modbus_server: фиксируют ожидаемое поведение на воспроизводимых входных данных.
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
* настройками рабочего устройства.
*/
#include <stdio.h>
#include <string.h>
#include "pcan_id.h"
#include "pcan_modbus_server.h"
static uint16_t holding[4096]; /* Полная 12-битная карта позволяет проверить последний допустимый адрес 0xFFF. */
static uint16_t input[4096]; /* Входной банк имеет тот же размер 4096 адресов, что поле StrAdr. */
static uint16_t coils = 0x000AU; /* Чередующиеся выходы 1 и 3 отличают функцию Coil=1 от Discrete=2. */
static uint16_t discretes = 0x0014U; /* Входы 2 и 4 установлены для проверки сохранённого битового чтения типа 0x5. */
static bool read_word(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t *value)
{
(void)user;
if (address >= 4096U) return false; /* Provider не принимает адреса вне 12-битной карты. */
if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
else if (bank == PCAN_MODBUS_INPUT) *value = input[address];
else if (bank == PCAN_MODBUS_DISCRETE && address < 16U) *value = (uint16_t)((discretes >> address) & 1U); /* Не сдвигаем 16-битное поле на недопустимое число позиций. */
else if (bank == PCAN_MODBUS_COIL && address < 16U) *value = (uint16_t)((coils >> address) & 1U); /* Функция 1 читает независимую маску выходов. */
else return false;
return true;
}
static bool write_word(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t value)
{
(void)user;
if (address >= 4096U) return false; /* Запись ограничена тем же адресным пространством, что чтение. */
if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
else if (bank == PCAN_MODBUS_COIL && address < 16U) coils = (uint16_t)((coils & ~(1U << address)) | ((value & 1U) << address)); /* Один callback изменяет ровно выбранный выход. */
else return false; /* Входные банки Discrete/Input недоступны для записи. */
return true;
}
static pcan_frame_t request(uint8_t function, uint16_t address, uint8_t count) /* Функция задаёт банк независимо от единого MsgType=0x5. */
{
pcan_id_t id = {0};
pcan_frame_t frame = {0};
id.priority = PCAN_PRIORITY_STANDARD;
id.route = PCAN_ROUTE_FROM_PM;
id.device_type = 7U;
id.device_id = 3U;
id.msg_type = PCAN_MSG_MODBUS; /* Все четыре функции используют один код сообщения 0x5. */
id.msg_body = pcan_body_modbus(address, count, function); /* Передаём физические значения функции и количества 1…4 общему упаковщику. */
frame.id = pcan_id_pack(&id);
frame.flags = PCAN_FLAG_IDE;
return frame;
}
#define CHECK(x) do { if (!(x)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); return 1; \
} } while (0)
int main(void)
{
pcan_modbus_server_t server;
pcan_frame_t req;
pcan_frame_t rsp;
pcan_id_t id;
memset(holding, 0, sizeof(holding));
memset(input, 0, sizeof(input));
input[10] = 0x1234U;
input[11] = 0xABCDU;
CHECK(pcan_modbus_server_init(&server, 7U, 3U, read_word, write_word, NULL));
req = request(PCAN_MODBUS_INPUT, 10U, 2U);
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
CHECK(rsp.dlc == 4U && rsp.data[0] == 0x34U && rsp.data[1] == 0x12U);
CHECK(rsp.data[2] == 0xCDU && rsp.data[3] == 0xABU);
pcan_id_unpack(rsp.id, &id);
CHECK(id.route == PCAN_ROUTE_FROM_DEVICE);
req = request(PCAN_MODBUS_HOLDING, 3U, 2U);
req.dlc = 4U;
req.data[0] = 0x22U; req.data[1] = 0x11U;
req.data[2] = 0x44U; req.data[3] = 0x33U;
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U);
req = request(PCAN_MODBUS_DISCRETE, 2U, 3U); /* Читаем три входа 2…4 через сохранённый тип 0x5. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
CHECK(rsp.dlc == 1U && rsp.data[0] == 0x05U); /* Три входа передаются младшими битами одного октета. */
req.id = (req.id & ~0xF0000UL) | ((uint32_t)PCAN_MSG_GAS_SLOT << 16U); /* Подменяем MsgType на независимый GAS SLOT 0x4, сохраняя остальные поля. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_NOT_FOR_US); /* Слотный кадр не должен вызывать Modbus provider и менять его счётчики. */
req = request(PCAN_MODBUS_INPUT, 0xFFFU, 2U); /* Двухрегистровое окно с последнего адреса должно быть отвергнуто до обращения к карте. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
CHECK(server.requests == 4U && server.responses == 3U && server.rejected == 1U);
for (uint8_t function = 1U; function <= 4U; ++function) { /* Покрываем все четыре банка единого сообщения. */
for (uint8_t count = 1U; count <= 4U; ++count) { /* Каждая функция проверяется с каждым допустимым количеством. */
uint16_t expected = (uint16_t)(0x20U | ((function - 1U) << 2U) | (count - 1U)); /* Адрес 2 остаётся в старших 12 битах, функция и количество независимо занимают по два бита. */
CHECK(pcan_body_modbus(2U, count, function) == expected); /* Проверяем эталонную битовую раскладку всех 16 сочетаний. */
CHECK(pcan_modbus_count(expected) == count && pcan_modbus_function(expected) == function); /* Декодирование возвращает исходные физические значения 1…4. */
req = request(function, 2U, count); /* Запрашиваем окно в пределах тестовых битовых и регистровых банков. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE); /* Все допустимые сочетания обслуживаются сервером. */
CHECK(rsp.dlc == (function <= 2U ? 1U : count * 2U)); /* Функции 1/2 возвращают октет, 3/4 — два октета на слово. */
pcan_id_unpack(rsp.id, &id); /* Ответ должен сохранять банк и количество исходного запроса. */
CHECK(id.msg_type == PCAN_MSG_MODBUS && id.msg_body == expected && id.route == PCAN_ROUTE_FROM_DEVICE); /* Меняется только направление сообщения. */
if (function <= 2U) CHECK(rsp.data[0] == (((function == 1U ? coils : discretes) >> 2U) & ((1U << count) - 1U))); /* Сверяем каждый бит с независимым источником банка. */
} /* Завершаем перебор количеств для текущей функции. */
} /* Проверены все шестнадцать вариантов. */
input[4095] = 0xFEDCU; /* Последний адрес карты содержит отличимое 16-битное значение. */
req = request(PCAN_MODBUS_INPUT, 4095U, 1U); /* Окно из одного регистра на границе допустимо. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE && rsp.data[0] == 0xDCU && rsp.data[1] == 0xFEU); /* Верхняя граница и little-endian проверяются одновременно. */
req = request(PCAN_MODBUS_COIL, 2U, 4U); /* Записываем четыре последовательных выхода отдельными битами. */
req.dlc = 1U; req.data[0] = 0x09U; /* Выходы addr+0 и addr+3 включены, промежуточные выключены. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE && ((coils >> 2U) & 15U) == 9U); /* Provider получает правильные адреса и значения каждого выхода. */
req.data[0] = 0x10U; /* Пятый бит не входит в заявленное четырёхбитное окно. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE && ((coils >> 2U) & 15U) == 9U); /* Неверная маска отклоняется до изменения выходов. */
req = request(PCAN_MODBUS_HOLDING, 3U, 2U); req.dlc = 3U; /* Двум регистрам нужны четыре октета; три являются неполным payload. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE && holding[3] == 0x1122U); /* Неверная длина не меняет уже записанные значения. */
for (uint8_t function = 2U; function <= 4U; function += 2U) { /* Функции входных банков 2 и 4 доступны только для чтения. */
req = request(function, 2U, 1U); req.dlc = function == 2U ? 1U : 2U; /* Подготавливаем формально правильный payload записи readonly-банка. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE); /* Сервер отвергает запись независимо от длины данных. */
} /* Оба входных банка защищены. */
req = request(PCAN_MODBUS_HOLDING, 0U, 1U); req.flags |= PCAN_FLAG_RTR; /* Remote frame не несёт разрешённого data payload ProtoCAN. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE); /* RTR отвергается до чтения банка. */
for (uint8_t type = 6U; type <= 7U; ++type) { /* Старые отдельные типы Holding/Input освобождены. */
req = request(PCAN_MODBUS_HOLDING, 0U, 1U); req.id = (req.id & ~0xF0000UL) | ((uint32_t)type << 16U); /* Сохраняем адрес и Body, изменяя только номер сервиса. */
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_NOT_FOR_US); /* Освобождённые типы не маршрутизируются в Modbus. */
} /* Проверены оба освобождённых номера. */
return 0;
}