Files
templates/c/protocan-transport/tests/test_gui.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

375 lines
13 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.
/**
* Тесты GUI-протокола и каталога общего адресного пространства.
* Эталонные байты совпадают с SETGUI/tests/test_gas_catalog.py.
*
* clang -std=c99 -Wall -Wextra -Iinclude tests/test_gui.c src/pcan_*.c src/gui_*.c -o test_gui
*/
#include <stdio.h>
#include <string.h>
#include "gui_catalog.h"
#include "gui_frame.h"
#include "pcan_gas.h"
static int g_fail;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
g_fail++; \
} \
} while (0)
static void dump(const char *tag, const uint8_t *data, size_t len)
{
printf("%s:", tag);
for (size_t i = 0; i < len; i++) {
printf(" %02X", data[i]);
}
printf("\n");
}
/* -- Транспорт ------------------------------------------------------------- */
static void test_crc32(void)
{
/* Контрольное значение CRC32/ISO-HDLC, оно же zlib.crc32. */
CHECK(gui_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL);
CHECK(gui_crc32((const uint8_t *)"", 0) == 0U);
}
static void test_ping_reference(void)
{
/* Эталон из PROJECT_RELATIONS.md:
A5 5A 01 01 12 34 00 00 EE 89 8C 9E */
static const uint8_t want[] = {
0xA5, 0x5A, 0x01, 0x01, 0x12, 0x34, 0x00, 0x00, 0xEE, 0x89, 0x8C, 0x9E
};
uint8_t out[GUI_FRAME_MAX];
size_t n = gui_frame_encode(GUI_MSG_PING, 0x1234, NULL, 0, out, sizeof out);
if ((n != sizeof want) || (memcmp(out, want, sizeof want) != 0)) {
dump("получено", out, n);
dump("ожидалось", want, sizeof want);
}
CHECK(n == sizeof want);
CHECK(memcmp(out, want, sizeof want) == 0);
}
static int s_rx_count;
static uint8_t s_rx_type;
static uint16_t s_rx_seq;
static uint16_t s_rx_size;
static uint8_t s_rx_payload[64];
static void on_frame(const gui_frame_t *frame, void *user)
{
(void)user;
s_rx_count++;
s_rx_type = frame->type;
s_rx_seq = frame->sequence;
s_rx_size = frame->size;
if (frame->size <= sizeof s_rx_payload) {
memcpy(s_rx_payload, frame->payload, frame->size);
}
}
static void test_parser(void)
{
uint8_t payload[5] = { 1, 2, 3, 4, 5 };
uint8_t out[GUI_FRAME_MAX];
size_t n = gui_frame_encode(GUI_MSG_GAS_WATCH_DATA, 0, payload, 5, out, sizeof out);
gui_parser_t p;
gui_parser_init(&p);
s_rx_count = 0;
/* Мусор и оборванная сигнатура перед настоящим кадром. */
static const uint8_t junk[] = { 0x00, 0xFF, 0xA5, 0xA5, 0x5A, 0x02 };
gui_parser_feed(&p, junk, sizeof junk, on_frame, NULL);
gui_parser_feed(&p, out, n, on_frame, NULL);
CHECK(s_rx_count == 1);
CHECK(s_rx_type == GUI_MSG_GAS_WATCH_DATA);
CHECK(s_rx_size == 5U);
CHECK(memcmp(s_rx_payload, payload, 5) == 0);
CHECK(p.stats.version_errors == 1U); /* 0x02 вместо версии 0x01 */
CHECK(p.stats.frames == 1U);
/* Битый CRC отвергается. */
out[n - 1U] ^= 0xFFU;
gui_parser_init(&p);
s_rx_count = 0;
gui_parser_feed(&p, out, n, on_frame, NULL);
CHECK(s_rx_count == 0);
CHECK(p.stats.crc_errors == 1U);
}
static void test_max_payload(void)
{
static uint8_t payload[GUI_MAX_PAYLOAD];
for (size_t i = 0; i < sizeof payload; i++) {
payload[i] = (uint8_t)i;
}
static uint8_t out[GUI_FRAME_MAX];
size_t n = gui_frame_encode(GUI_MSG_WRITE_REGISTERS, 0xFFFF, payload,
GUI_MAX_PAYLOAD, out, sizeof out);
CHECK(n == GUI_FRAME_MAX);
CHECK(out[6] == 0x02U && out[7] == 0x00U); /* size = 512 big-endian */
/* Больше максимума не кодируем и буфер не переполняем. */
CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, GUI_MAX_PAYLOAD + 1U,
out, sizeof out) == 0U);
CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, 100U, out, 50U) == 0U);
}
/* -- Каталог --------------------------------------------------------------- */
static uint16_t s_settings[4];
static uint16_t s_diag[4] = { 0x0100, 0x003C, 0x0000, 0x0002 };
static const pcan_gas_region_t s_regions[] = {
{ 0x0000U, 4U, s_settings, NULL, NULL, 0U, NULL, "settings" },
{ 0xFF00U, 4U, s_diag, NULL, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
};
static const pcan_gas_map_t s_map = { s_regions, 2U };
static const gui_object_t s_objects[] = {
{ 0xFF01U, GUI_OBJ_U16, GUI_OBJ_READABLE | GUI_OBJ_DEFAULT_WATCH,
0, GUI_UNIT_SECOND, "Uptime" },
{ 0x0000U, GUI_OBJ_I16, GUI_OBJ_READABLE | GUI_OBJ_WRITABLE,
-2, GUI_UNIT_VOLT, "Uставка" },
{ 0xFF02U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, GUI_UNIT_COUNT, "Кадров" },
};
static const gui_catalog_t s_catalog = { s_objects, 3U };
static void test_catalog_validate(void)
{
CHECK(gui_catalog_validate(&s_catalog, &s_map));
/* Адрес вне карты GAS. */
static const gui_object_t bad_addr[] = {
{ 0x8000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "нет" },
};
const gui_catalog_t c1 = { bad_addr, 1U };
CHECK(!gui_catalog_validate(&c1, &s_map));
/* U32 требует двух подряд идущих адресов; 0xFF03 есть, 0xFF04 - нет. */
static const gui_object_t split[] = {
{ 0xFF03U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, 0, "хвост" },
};
const gui_catalog_t c2 = { split, 1U };
CHECK(!gui_catalog_validate(&c2, &s_map));
/* Имя длиннее поля: 13 кириллических символов - это 26 байт. */
static const gui_object_t long_name[] = {
{ 0x0000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "тринадцать сим" },
};
const gui_catalog_t c3 = { long_name, 1U };
CHECK(!gui_catalog_validate(&c3, &s_map));
}
static void test_catalog_encode(void)
{
uint8_t out[GUI_MAX_PAYLOAD];
size_t n = gui_catalog_encode(&s_catalog, 0U, 1U, out, sizeof out);
CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE);
CHECK(gui_get_u16(&out[0]) == 3U); /* total */
CHECK(gui_get_u16(&out[2]) == 0U); /* start_index */
CHECK(gui_get_u16(&out[4]) == 1U); /* count */
/* Запись Uptime совпадает с эталоном из Python-тестов. */
static const uint8_t want[] = {
0x01, 0xFF, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00,
'U', 'p', 't', 'i', 'm', 'e', 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
if (memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) != 0) {
dump("запись", &out[GUI_CATALOG_HEADER], GUI_ENTRY_SIZE);
}
CHECK(sizeof want == GUI_ENTRY_SIZE);
CHECK(memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) == 0);
/* Отрицательный масштаб кодируется как int8. */
n = gui_catalog_encode(&s_catalog, 1U, 1U, out, sizeof out);
CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE);
CHECK(out[GUI_CATALOG_HEADER + 4U] == 0xFEU); /* -2 */
/* max_count = 0 отдаёт всё, что влезает. */
n = gui_catalog_encode(&s_catalog, 0U, 0U, out, sizeof out);
CHECK(n == GUI_CATALOG_HEADER + (3U * GUI_ENTRY_SIZE));
CHECK(gui_get_u16(&out[4]) == 3U);
/* Индекс за концом каталога - пустой ответ, а не мусор. */
CHECK(gui_catalog_encode(&s_catalog, 3U, 0U, out, sizeof out) == 0U);
/* Маленький буфер отвергается. */
CHECK(gui_catalog_encode(&s_catalog, 0U, 1U, out, 8U) == 0U);
CHECK(GUI_ENTRIES_PER_FRAME == 15U);
}
/* -- Подписка -------------------------------------------------------------- */
static void test_watch(void)
{
gui_watch_t watch;
gui_watch_init(&watch);
/* period = 100, два адреса: один есть в карте, второй нет. */
static const uint8_t request[] = {
0x64, 0x00, 0x02, 0x00, 0x01, 0xFF, 0x00, 0x80
};
uint16_t accepted = gui_watch_apply(&watch, &s_map, request, sizeof request);
CHECK(accepted == 1U);
CHECK(watch.count == 1U);
CHECK(watch.address[0] == 0xFF01U);
CHECK(watch.period_ms == 100U);
uint8_t ack[4];
CHECK(gui_watch_encode_ack(&watch, ack, sizeof ack) == 4U);
CHECK(gui_get_u16(&ack[0]) == 100U);
CHECK(gui_get_u16(&ack[2]) == 1U);
/* Значение берётся из карты GAS. */
uint8_t data[GUI_MAX_PAYLOAD];
size_t n = gui_watch_encode_data(&watch, &s_map, 0x12345678UL, data, sizeof data);
CHECK(n == 8U);
CHECK(data[0] == 0x78U && data[3] == 0x12U); /* время little-endian */
CHECK(gui_get_u16(&data[4]) == 1U);
CHECK(gui_get_u16(&data[6]) == 0x003CU); /* s_diag[1] = uptime */
/* Битый payload не должен ломать действующую подписку. */
static const uint8_t broken[] = { 0x64, 0x00, 0x05, 0x00, 0x01, 0xFF };
CHECK(gui_watch_apply(&watch, &s_map, broken, sizeof broken) == 0xFFFFU);
CHECK(watch.count == 1U);
CHECK(watch.period_ms == 100U);
/* Слишком длинная подписка отвергается целиком. */
static uint8_t huge[4U + ((GUI_WATCH_MAX + 1U) * 2U)];
gui_put_u16(&huge[0], 10U);
gui_put_u16(&huge[2], GUI_WATCH_MAX + 1U);
CHECK(gui_watch_apply(&watch, &s_map, huge, sizeof huge) == 0xFFFFU);
CHECK(watch.count == 1U);
/* period = 0 останавливает поток. */
static const uint8_t stop[] = { 0x00, 0x00, 0x00, 0x00 };
CHECK(gui_watch_apply(&watch, &s_map, stop, sizeof stop) == 0U);
CHECK(watch.period_ms == 0U);
CHECK(!gui_watch_due(&watch, 1000U));
CHECK(gui_watch_encode_data(&watch, &s_map, 0U, data, sizeof data) == 0U);
}
static void test_watch_timing(void)
{
gui_watch_t watch;
gui_watch_init(&watch);
static const uint8_t request[] = { 0x0A, 0x00, 0x01, 0x00, 0x01, 0xFF };
CHECK(gui_watch_apply(&watch, &s_map, request, sizeof request) == 1U);
watch.next_ms = 1000U;
CHECK(!gui_watch_due(&watch, 999U));
CHECK(gui_watch_due(&watch, 1000U));
CHECK(gui_watch_due(&watch, 1500U));
/* Идём в такт, пока успеваем. */
gui_watch_advance(&watch, 1000U);
CHECK(watch.next_ms == 1010U);
/* Отстали сильно - не нагоняем пачкой, а идём от текущего момента. */
watch.next_ms = 1000U;
gui_watch_advance(&watch, 5000U);
CHECK(watch.next_ms == 5010U);
/* Переполнение счётчика миллисекунд не должно вешать поток. */
watch.next_ms = 0xFFFFFFF0UL;
CHECK(!gui_watch_due(&watch, 0xFFFFFFE0UL));
CHECK(gui_watch_due(&watch, 0x00000010UL));
}
/* -- Канал ----------------------------------------------------------------- */
static uint8_t s_line[1024];
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 void test_link(void)
{
pcan_io_t io = { fake_write, fake_space, NULL };
static gui_link_t link;
CHECK(gui_link_init(&link, &io, on_frame, NULL));
s_line_len = 0U;
s_rx_count = 0;
uint8_t payload[3] = { 0xDE, 0xAD, 0xBE };
CHECK(gui_link_send(&link, GUI_MSG_GAS_CATALOG, 7U, payload, 3U));
CHECK(link.tx_frames == 1U);
/* Поток обратно кусками произвольной длины. */
size_t off = 0U;
while (off < s_line_len) {
size_t chunk = ((s_line_len - off) > 4U) ? 4U : (s_line_len - off);
gui_link_feed(&link, &s_line[off], chunk);
off += chunk;
}
CHECK(s_rx_count == 1);
CHECK(s_rx_type == GUI_MSG_GAS_CATALOG);
CHECK(s_rx_seq == 7U);
CHECK(s_rx_size == 3U);
/* NACK - два байта кода результата. */
s_line_len = 0U;
s_rx_count = 0;
CHECK(gui_link_nack(&link, 9U, GUI_RESULT_NOT_FOUND));
gui_link_feed(&link, s_line, s_line_len);
CHECK(s_rx_count == 1);
CHECK(s_rx_type == GUI_MSG_NACK);
CHECK(gui_get_u16(s_rx_payload) == GUI_RESULT_NOT_FOUND);
/* Не хватило места - кадр не уходит обрывком. */
s_line_space = 8U;
s_line_len = 0U;
CHECK(!gui_link_can_send(&link, 100U));
CHECK(!gui_link_send(&link, GUI_MSG_PING, 1U, payload, 3U));
CHECK(s_line_len == 0U);
CHECK(link.tx_dropped == 1U);
s_line_space = sizeof s_line;
}
int main(void)
{
test_crc32();
test_ping_reference();
test_parser();
test_max_payload();
test_catalog_validate();
test_catalog_encode();
test_watch();
test_watch_timing();
test_link();
if (g_fail == 0) {
printf("all gui tests passed\n");
return 0;
}
printf("%d check(s) failed\n", g_fail);
return 1;
}