/* * Проверки modbus_server: фиксируют ожидаемое поведение на воспроизводимых входных данных. * Тестовые значения и подмены окружения относятся к проверке контракта; они не являются * настройками рабочего устройства. */ #include #include #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; }