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 проходят.
This commit is contained in:
2026-08-23 01:15:35 +03:00
parent 873ac438f3
commit 3dc636e012
27 changed files with 3646 additions and 0 deletions

View File

@@ -0,0 +1,445 @@
/**
* Тесты библиотеки. Платформенных зависимостей нет, собирается любым 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;
}