Files
templates/c/protocan-transport/tests/test_transport.c
Andrey Kruchinkin 3dc636e012 feat(protocan-transport): транспортный уровень ProtoCAN и каталог GUI
Перенесён из репозитория protocan-transport, который подключался
сабмодулем в CAN_to_RS485.

Кадрирование AA 55 с CRC16 поверх любого байтового потока (RS485, RS232,
USB CDC), разбор 29-битного идентификатора, общее адресное пространство
регистров и каталог с подпиской на поток значений для SETGUI. Состояние
живёт в структурах вызывающего, поэтому в одной прошивке поднимается
сколько угодно независимых каналов. Порт STM32F4 (USART + DMA) в комплекте.

Хостовые тесты test_transport и test_gui проходят.
2026-08-23 01:15:35 +03:00

446 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Тесты библиотеки. Платформенных зависимостей нет, собирается любым C99:
*
* clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test
*/
#include <stdio.h>
#include <string.h>
#include "protocan_transport.h"
static int g_fail;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
g_fail++; \
} \
} while (0)
/* -- CRC ------------------------------------------------------------------- */
static void test_crc(void)
{
/* Контрольное значение CRC-16/CCITT-FALSE. */
CHECK(pcan_crc16((const uint8_t *)"123456789", 9) == 0x29B1U);
}
/* -- Кадр ------------------------------------------------------------------ */
static void test_encode(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.seq = 1U;
f.flags = PCAN_FLAG_IDE;
f.id = 0x1234567UL;
f.dlc = 2U;
f.data[0] = 0xAAU;
f.data[1] = 0xBBU;
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
/* Эталон согласован с gui/transport.py. */
static const uint8_t want[] = {
0xAA, 0x55, 0x08, 0x01, 0x01, 0x67, 0x45, 0x23, 0x01, 0xAA, 0xBB,
0xFE, 0x14
};
CHECK(n == sizeof want);
CHECK(memcmp(raw, want, sizeof want) == 0);
/* Маленький буфер должен отвергаться, а не портить память. */
CHECK(pcan_frame_encode(&f, raw, 4U) == 0U);
}
static void test_encode_max(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.seq = 255U;
f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR;
f.id = 0x1FFFFFFFUL;
f.dlc = 8U;
for (int i = 0; i < 8; i++) {
f.data[i] = (uint8_t)(0xF0 + i);
}
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
static const uint8_t want[] = {
0xAA, 0x55, 0x0E, 0xFF, 0x05, 0xFF, 0xFF, 0xFF, 0x1F,
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0x6D, 0x0B
};
CHECK(n == sizeof want);
CHECK(n == PCAN_FRAME_MAX);
CHECK(memcmp(raw, want, sizeof want) == 0);
}
static void test_roundtrip(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR;
f.id = 0x0ABCDEFUL;
f.dlc = 5U;
memcpy(f.data, "HELLO", 5);
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
pcan_parser_t p;
pcan_parser_init(&p);
pcan_frame_t out;
memset(&out, 0, sizeof out);
int got = 0;
for (size_t i = 0; i < n; i++) {
if (pcan_parser_push(&p, raw[i], &out)) {
got++;
}
}
CHECK(got == 1);
CHECK(out.id == f.id);
CHECK(out.dlc == f.dlc);
CHECK(out.flags == f.flags);
CHECK(memcmp(out.data, "HELLO", 5) == 0);
/* Хвост за DLC должен быть обнулён, а не содержать прошлый кадр. */
CHECK(out.data[5] == 0U && out.data[6] == 0U && out.data[7] == 0U);
CHECK(p.stats.frames == 1U);
}
static void test_resync(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE;
f.id = 0x100UL;
f.dlc = 1U;
f.data[0] = 0x42U;
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
/* Мусор, оборванная сигнатура и битый LEN перед настоящим кадром. */
static const uint8_t junk[] = { 0x00, 0xFF, 0xAA, 0xAA, 0x55, 0x03, 0x11, 0xAA };
pcan_parser_t p;
pcan_parser_init(&p);
pcan_frame_t out;
int got = 0;
for (size_t i = 0; i < sizeof junk; i++) {
if (pcan_parser_push(&p, junk[i], &out)) {
got++;
}
}
for (size_t i = 0; i < n; i++) {
if (pcan_parser_push(&p, raw[i], &out)) {
got++;
}
}
CHECK(got == 1);
CHECK(out.id == 0x100UL);
CHECK(out.data[0] == 0x42U);
CHECK(p.stats.bad_len == 1U);
}
static void test_bad_crc(void)
{
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE;
f.id = 0x55UL;
f.dlc = 3U;
uint8_t raw[PCAN_FRAME_MAX];
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
raw[n - 1U] ^= 0xFFU;
pcan_parser_t p;
pcan_parser_init(&p);
pcan_frame_t out;
int got = 0;
for (size_t i = 0; i < n; i++) {
if (pcan_parser_push(&p, raw[i], &out)) {
got++;
}
}
CHECK(got == 0);
CHECK(p.stats.crc_errors == 1U);
}
/* -- Идентификатор --------------------------------------------------------- */
static void test_id(void)
{
pcan_id_t id = { 0x1234U, PCAN_MSG_GAS, 2U, 1U,
PCAN_ROUTE_FROM_DEVICE, PCAN_PRIORITY_STANDARD };
uint32_t raw = pcan_id_pack(&id);
CHECK(raw == 0x19231234UL);
pcan_id_t back;
pcan_id_unpack(raw, &back);
CHECK(back.msg_body == id.msg_body);
CHECK(back.msg_type == id.msg_type);
CHECK(back.device_id == id.device_id);
CHECK(back.device_type == id.device_type);
CHECK(back.route == id.route);
CHECK(back.priority == id.priority);
/* Раскладки MsgBody из protocan.h */
CHECK(pcan_body_broadcast(3U, 0U) == 0x0030U);
CHECK(pcan_body_typed(4U, 17U) == 0x4011U);
CHECK(pcan_body_modbus(0x100U, 3U) == 0x1003U);
CHECK(pcan_body_error(0U, 0xFFU) == 0x00FFU);
}
/* -- Кольцевой буфер ------------------------------------------------------- */
static void test_ring(void)
{
uint8_t storage[16];
pcan_ring_t rb;
CHECK(pcan_ring_init(&rb, storage, 16U));
/* Размер не степень двойки - отказ, а не тихая порча индексов. */
pcan_ring_t bad;
CHECK(!pcan_ring_init(&bad, storage, 15U));
CHECK(pcan_ring_free(&rb) == 15U);
CHECK(pcan_ring_write(&rb, (const uint8_t *)"0123456789", 10U));
CHECK(pcan_ring_count(&rb) == 10U);
/* Не влезает - не должно попасть ни одного байта. */
CHECK(!pcan_ring_write(&rb, (const uint8_t *)"ABCDEF", 6U));
CHECK(pcan_ring_count(&rb) == 10U);
CHECK(rb.dropped == 6U);
/* Линейный участок для DMA. */
const uint8_t *ptr = NULL;
uint16_t lin = pcan_ring_linear(&rb, &ptr);
CHECK(lin == 10U);
CHECK(ptr == storage);
pcan_ring_consume(&rb, lin);
CHECK(pcan_ring_empty(&rb));
/* Заворот: линейный участок обязан обрываться на конце буфера. */
CHECK(pcan_ring_write(&rb, (const uint8_t *)"ABCDEFGH", 8U));
lin = pcan_ring_linear(&rb, &ptr);
CHECK(lin == 6U); /* от tail=10 до конца буфера (16) */
CHECK(ptr == &storage[10]);
pcan_ring_consume(&rb, lin);
lin = pcan_ring_linear(&rb, &ptr);
CHECK(lin == 2U);
CHECK(ptr == storage);
}
/* -- Общее адресное пространство ------------------------------------------- */
static uint16_t s_holding[8];
static uint16_t s_computed_calls;
static pcan_gas_status_t computed_read(const pcan_gas_region_t *r,
uint16_t offset, uint16_t *value)
{
(void)r;
s_computed_calls++;
*value = (uint16_t)(0xA000U + offset);
return PCAN_GAS_OK;
}
static const pcan_gas_region_t s_regions[] = {
{ 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, "holding" },
{ 0xFF00U, 4U, NULL, computed_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
};
static const pcan_gas_map_t s_map = { s_regions, 2U };
static void test_gas(void)
{
CHECK(pcan_gas_map_validate(&s_map));
/* Перекрытие регионов должно отвергаться. */
static const pcan_gas_region_t overlap[] = {
{ 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL },
{ 0x0004U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL },
};
const pcan_gas_map_t bad = { overlap, 2U };
CHECK(!pcan_gas_map_validate(&bad));
uint16_t v = 0U;
CHECK(pcan_gas_write(&s_map, 0x0002U, 0x1234U) == PCAN_GAS_OK);
CHECK(pcan_gas_read(&s_map, 0x0002U, &v) == PCAN_GAS_OK);
CHECK(v == 0x1234U);
CHECK(s_holding[2] == 0x1234U);
/* Дырка в карте. */
CHECK(pcan_gas_read(&s_map, 0x0100U, &v) == PCAN_GAS_NO_REG);
/* Регион только для чтения. */
CHECK(pcan_gas_write(&s_map, 0xFF00U, 1U) == PCAN_GAS_READ_ONLY);
/* Колбэк вместо памяти. */
CHECK(pcan_gas_read(&s_map, 0xFF02U, &v) == PCAN_GAS_OK);
CHECK(v == 0xA002U);
CHECK(s_computed_calls == 1U);
/* Блок обрывается на первом адресе вне карты. */
uint16_t block[8];
CHECK(pcan_gas_read_block(&s_map, 0x0006U, block, 8U) == 2U);
}
static void test_gas_frames(void)
{
for (int i = 0; i < 8; i++) {
s_holding[i] = (uint16_t)(0x1000 + i);
}
/* Запрос на чтение: GAS, DLC = 0, адрес в MsgBody. */
pcan_id_t id = { 0x0002U, PCAN_MSG_GAS, 2U, 1U,
PCAN_ROUTE_FROM_PM, PCAN_PRIORITY_STANDARD };
pcan_frame_t req;
memset(&req, 0, sizeof req);
req.id = pcan_id_pack(&id);
req.flags = PCAN_FLAG_IDE;
req.dlc = 0U;
pcan_frame_t rsp;
memset(&rsp, 0, sizeof rsp);
CHECK(pcan_gas_handle(&s_map, &req, &rsp));
CHECK(rsp.dlc == 8U); /* 4 регистра */
pcan_id_t rid;
pcan_id_unpack(rsp.id, &rid);
CHECK(rid.msg_type == PCAN_MSG_GAS);
CHECK(rid.msg_body == 0x0002U);
CHECK(rid.route == PCAN_ROUTE_FROM_DEVICE);
/* Регистры лежат младшим байтом вперёд - как в SETCAN. */
CHECK(rsp.data[0] == 0x02U && rsp.data[1] == 0x10U);
uint16_t regs[4];
CHECK(pcan_gas_frame_to_regs(&rsp, regs, 4U) == 4U);
CHECK(regs[0] == 0x1002U && regs[3] == 0x1005U);
/* Запись: GAS с данными, ответа быть не должно. */
pcan_frame_t wr;
memset(&wr, 0, sizeof wr);
id.msg_body = 0x0000U;
wr.id = pcan_id_pack(&id);
wr.flags = PCAN_FLAG_IDE;
wr.dlc = 4U;
wr.data[0] = 0xEFU; wr.data[1] = 0xBEU;
wr.data[2] = 0xADU; wr.data[3] = 0xDEU;
CHECK(!pcan_gas_handle(&s_map, &wr, &rsp));
CHECK(s_holding[0] == 0xBEEFU);
CHECK(s_holding[1] == 0xDEADU);
/* Чтение по адресу вне карты не должно порождать ответ. */
id.msg_body = 0x0500U;
req.id = pcan_id_pack(&id);
CHECK(!pcan_gas_handle(&s_map, &req, &rsp));
}
/* -- Канал связи ----------------------------------------------------------- */
static uint8_t s_line[256];
static size_t s_line_len;
static size_t s_line_space = sizeof s_line;
static size_t fake_write(void *ctx, const uint8_t *data, size_t len)
{
(void)ctx;
if ((s_line_len + len) > s_line_space) {
return 0U; /* всё-или-ничего */
}
memcpy(&s_line[s_line_len], data, len);
s_line_len += len;
return len;
}
static size_t fake_space(void *ctx)
{
(void)ctx;
return s_line_space - s_line_len;
}
static int s_rx_count;
static pcan_frame_t s_rx_last;
static void on_frame(const pcan_frame_t *f, void *user)
{
(void)user;
s_rx_count++;
s_rx_last = *f;
}
static void test_link(void)
{
pcan_io_t io = { fake_write, fake_space, NULL };
pcan_link_t link;
CHECK(pcan_link_init(&link, &io, on_frame, NULL));
s_line_len = 0U;
s_rx_count = 0;
/* SEQ должен инкрементироваться сам. */
for (int i = 0; i < 3; i++) {
pcan_frame_t f;
memset(&f, 0, sizeof f);
f.flags = PCAN_FLAG_IDE;
f.id = (uint32_t)(0x200 + i);
f.dlc = 1U;
f.data[0] = (uint8_t)i;
CHECK(pcan_link_send(&link, &f));
CHECK(f.seq == (uint8_t)i);
}
CHECK(link.tx.tx_frames == 3U);
/* Тот же поток скармливаем обратно, по кускам произвольной длины. */
size_t off = 0U;
while (off < s_line_len) {
size_t chunk = ((s_line_len - off) > 5U) ? 5U : (s_line_len - off);
pcan_link_feed(&link, &s_line[off], chunk);
off += chunk;
}
CHECK(s_rx_count == 3);
CHECK(s_rx_last.id == 0x202UL);
CHECK(s_rx_last.seq == 2U);
CHECK(pcan_link_rx_stats(&link)->frames == 3U);
/* Переполнение очереди: кадр не должен уйти обрывком. */
s_line_space = 4U;
s_line_len = 0U;
pcan_frame_t big;
memset(&big, 0, sizeof big);
big.flags = PCAN_FLAG_IDE;
big.dlc = 8U;
uint8_t seq_before = link.tx_seq;
CHECK(!pcan_link_send(&link, &big));
CHECK(s_line_len == 0U);
CHECK(link.tx.tx_dropped == 1U);
/* SEQ не двигается на неотправленном кадре. */
CHECK(link.tx_seq == seq_before);
CHECK(!pcan_link_can_send(&link));
s_line_space = sizeof s_line;
}
int main(void)
{
test_crc();
test_encode();
test_encode_max();
test_roundtrip();
test_resync();
test_bad_crc();
test_id();
test_ring();
test_gas();
test_gas_frames();
test_link();
if (g_fail == 0) {
printf("all tests passed\n");
return 0;
}
printf("%d check(s) failed\n", g_fail);
return 1;
}