131 lines
11 KiB
C
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;
|
|
}
|