Перенесён из репозитория protocan-transport, который подключался сабмодулем в CAN_to_RS485. Кадрирование AA 55 с CRC16 поверх любого байтового потока (RS485, RS232, USB CDC), разбор 29-битного идентификатора, общее адресное пространство регистров и каталог с подпиской на поток значений для SETGUI. Состояние живёт в структурах вызывающего, поэтому в одной прошивке поднимается сколько угодно независимых каналов. Порт STM32F4 (USART + DMA) в комплекте. Хостовые тесты test_transport и test_gui проходят.
375 lines
13 KiB
C
375 lines
13 KiB
C
/**
|
||
* Тесты 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;
|
||
}
|