diff --git a/makefile b/makefile index d00dc12..3470402 100644 --- a/makefile +++ b/makefile @@ -20,7 +20,7 @@ PLUTO_B ?= 192.168.3.1 BIN := build SRC := src -all: $(BIN)/transmitter $(BIN)/receiver $(BIN)/ofdm_bench +all: $(BIN)/transmitter $(BIN)/receiver $(BIN)/ofdm_bench $(BIN)/udp_gw $(BIN): mkdir -p $(BIN) @@ -34,15 +34,27 @@ $(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/fra $(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN) $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS) +# UDP-туннель (roadmap §10 п.8) — самостоятельный бинарь, без iio/liquid/fec. +# Работает НА плате (piped с transmitter/receiver), поэтому та же кросс-сборка. +$(BIN)/udp_gw: $(SRC)/udp_gw.c | $(BIN) + $(CC) $(CFLAGS) $< -o $@ + # Оффлайн-самопроверка GF(256) — нативная сборка (gcc хоста, напр. RPi/aarch64) $(BIN)/gftest: $(SRC)/group_fec.c $(SRC)/group_fec.h | $(BIN) cc -O2 -Wall -DGF_SELFTEST $(SRC)/group_fec.c -o $@ +# Оффлайн-самопроверка парсера udp_gw — та же логика, нативная сборка. +$(BIN)/gwtest: $(SRC)/udp_gw.c | $(BIN) + cc -O2 -Wall -Wextra -DGW_SELFTEST $(SRC)/udp_gw.c -o $@ + tx: $(BIN)/transmitter rx: $(BIN)/receiver bench: $(BIN)/ofdm_bench +gw: $(BIN)/udp_gw gftest: $(BIN)/gftest $(BIN)/gftest +gwtest: $(BIN)/gwtest + $(BIN)/gwtest # --- деплой (scp -O: на прошивке v0.38 нет sftp-server) --- deploy: all @@ -64,13 +76,14 @@ clean: rm -rf $(BIN) help: - @echo "make [all] — собрать transmitter, receiver, ofdm_bench (static armhf)" - @echo "make tx|rx|bench — собрать отдельную цель" + @echo "make [all] — собрать transmitter, receiver, ofdm_bench, udp_gw (static armhf)" + @echo "make tx|rx|bench|gw — собрать отдельную цель" @echo "make gftest — оффлайн-самопроверка group-FEC (нативный cc)" + @echo "make gwtest — оффлайн-самопроверка парсера udp_gw (нативный cc)" @echo "make deploy — залить бинарники на обе платы (scp -O в /tmp)" @echo "make test — e2e-тест передачи файла с md5-сверкой" @echo "make check-env — проверить тулчейн и $(XARM)" @echo "make clean — удалить build/" @echo "переменные: PLUTO_A=$(PLUTO_A) PLUTO_B=$(PLUTO_B) XARM=$(XARM)" -.PHONY: all tx rx bench gftest deploy test check-env clean help \ No newline at end of file +.PHONY: all tx rx bench gw gftest gwtest deploy test check-env clean help \ No newline at end of file diff --git a/src/udp_gw.c b/src/udp_gw.c new file mode 100644 index 0000000..f297768 --- /dev/null +++ b/src/udp_gw.c @@ -0,0 +1,487 @@ +// udp_gw.c — UDP-туннель поверх байтового потока pluto-link +// (roadmap §10 п.8, COMPARISON.md §7 п.3). Самостоятельный POSIX-бинарь без +// iio/liquid/fec — работает по обе стороны линка как shell-фильтр: +// +// приём: receiver ... | udp_gw -d ip:port (кадры линка → датаграммы) +// отдача: udp_gw -l port | transmitter ... (датаграммы → кадры линка) +// +// Ядро линка (transmitter/receiver/group_fec/frame_tx) не меняется ни на +// байт: рекорды — это тонкий слой поверх уже существующего байтового потока +// stdin→stdout, свободно пересекающий границы 1024-байтных чанков. +// +// Формат рекорда (8 Б заголовка + payload ≤1472 Б, Ethernet MTU — см. +// CLAUDE.md «видео — raw UDP»): +// 0-1 magic 0xD5 0x5D | 2-3 len u16 BE | 4-5 id u16 BE | 6 флаги (0) | 7 hcrc8 +// Целостность payload гарантирует сам линк (CRC32-поверх-RS, frame_tx.c) — +// свой CRC данных не нужен; hcrc8 нужен только чтобы отличить настоящий magic +// от случайных байт 0xD5 0x5D в данных при ресинхронизации потока после дыры +// (радио-потеря кадра на симплексе рвёт границы рекордов произвольно). +// +// Самопроверка парсера (аналог group_fec.c/GF_SELFTEST, нативная сборка на +// хосте, без кросс-тулчейна): cc -O2 -Wall -DGW_SELFTEST src/udp_gw.c -o gwtest + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GW_MAGIC0 0xD5 +#define GW_MAGIC1 0x5D +#define GW_HDR 8 +#define GW_MAX_PAYLOAD 1472 // Ethernet MTU 1500 − IP 20 − UDP 8 +#define GW_MAX_RECORD (GW_HDR + GW_MAX_PAYLOAD) +#define GW_IO_CHUNK 8192 // размер одного read() в deliver-режиме + +// Буфер парсера: перед каждым push() в нём остаётся не более одного неполного +// рекорда (полные рекорды дренирует next() в том же цикле вызывающего) — то +// есть максимум GW_MAX_RECORD−1 байт хвоста + один read() ≤ GW_IO_CHUNK. +// Запас исключает штатное срабатывание аварийного ресинка в gw_parser_push. +#define GW_PARSE_BUF (GW_MAX_RECORD * 2 + GW_IO_CHUNK) + +// --- запись: pack/CRC8/парсер (общая часть, тестируется офлайн) ----------- + +static uint8_t gw_crc8(const uint8_t *d, size_t n) { + uint8_t c = 0; + for (size_t i = 0; i < n; i++) { + c ^= d[i]; + for (int b = 0; b < 8; b++) + c = (uint8_t)((c & 0x80) ? ((c << 1) ^ 0x07) : (c << 1)); + } + return c; +} + +// Упаковка одной датаграммы в запись. out ≥ GW_HDR + n. Возврат — длина записи. +static size_t gw_pack(uint8_t *out, uint16_t id, const uint8_t *pl, uint16_t n) { + out[0] = GW_MAGIC0; out[1] = GW_MAGIC1; + out[2] = (uint8_t)(n >> 8); out[3] = (uint8_t)(n & 0xFF); + out[4] = (uint8_t)(id >> 8); out[5] = (uint8_t)(id & 0xFF); + out[6] = 0; // флаги — резерв + out[7] = gw_crc8(out, 7); + memcpy(out + GW_HDR, pl, n); + return (size_t)GW_HDR + n; +} + +// Разбор потока записей с ресинхронизацией: линк — байтовый поток, потеря +// кадра на симплексе рвёт границы записей произвольно. magic+hcrc8 находит +// следующую настоящую запись, отбрасывая мусор между дырой и ней. +typedef struct { + uint8_t buf[GW_PARSE_BUF]; + size_t len, pos; // валидные байты [0,len); курсор сканирования + uint64_t records; // успешно извлечено + uint64_t resync_bytes; // байт пропущено при поиске magic (не-magic) + uint64_t bad_crc; // кандидатов magic отсеяно по hcrc8/длине + uint64_t gaps; // сумма пропущенных id (wrap/переупорядочение не считаются) + int have_last_id; + uint16_t last_id; +} gw_parser_t; + +static void gw_parser_init(gw_parser_t *p) { memset(p, 0, sizeof(*p)); } + +static void gw_compact(gw_parser_t *p) { + if (p->pos == 0) return; + memmove(p->buf, p->buf + p->pos, p->len - p->pos); + p->len -= p->pos; + p->pos = 0; +} + +// Добавляет сырые байты в буфер парсера, освобождая место компактированием. +// Штатно не переполняется (см. GW_PARSE_BUF); аварийный сдвиг на 1 байт — +// только защита от зависания на потоке без единого валидного magic. +static void gw_parser_push(gw_parser_t *p, const uint8_t *data, size_t n) { + while (n > 0) { + gw_compact(p); + if (p->len == sizeof(p->buf)) { + p->pos = 1; + gw_compact(p); + p->resync_bytes++; + } + size_t space = sizeof(p->buf) - p->len; + size_t take = n < space ? n : space; + memcpy(p->buf + p->len, data, take); + p->len += take; + data += take; n -= take; + } +} + +// Извлекает следующую запись. Возврат 1 — *id/*pl/*pl_len заполнены (pl — +// указатель ВНУТРЬ буфера парсера, валиден только до следующего push/next). +// Возврат 0 — данных пока не хватает, жди push. +static int gw_parser_next(gw_parser_t *p, uint16_t *id, const uint8_t **pl, uint16_t *pl_len) { + for (;;) { + while (p->pos + 1 < p->len && + !(p->buf[p->pos] == GW_MAGIC0 && p->buf[p->pos + 1] == GW_MAGIC1)) { + p->pos++; + p->resync_bytes++; + } + if (p->pos + GW_HDR > p->len) return 0; // заголовок ещё не весь пришёл + + const uint8_t *h = p->buf + p->pos; + if (gw_crc8(h, 7) != h[7]) { // ложный magic в данных + p->pos++; + p->bad_crc++; + continue; + } + uint16_t n = (uint16_t)(((uint16_t)h[2] << 8) | h[3]); + if (n > GW_MAX_PAYLOAD) { // валидный hcrc8, но нелепая длина — + p->pos++; // тоже ложное совпадение (редко, 1/256) + p->bad_crc++; + continue; + } + if (p->pos + GW_HDR + n > p->len) return 0; // тело записи ещё не пришло целиком + + uint16_t rid = (uint16_t)(((uint16_t)h[4] << 8) | h[5]); + if (p->have_last_id) { + uint16_t gap = (uint16_t)(rid - p->last_id - 1); + if (gap != 0 && gap < 0x8000) p->gaps += gap; // ≥0x8000 — переупорядочение/wrap, не дыра + } + p->have_last_id = 1; + p->last_id = rid; + p->records++; + + *id = rid; *pl = h + GW_HDR; *pl_len = n; + p->pos += (size_t)GW_HDR + n; + return 1; + } +} + +#ifndef GW_SELFTEST +// --- сетевой ввод-вывод ----------------------------------------------- + +static volatile sig_atomic_t g_stop = 0; +static void on_signal(int s) { (void)s; g_stop = 1; } + +static void setup_signals(void) { + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + signal(SIGPIPE, SIG_IGN); +} + +// Вход: UDP-датаграммы → записи в stdout. stdout в O_NONBLOCK: если приёмник +// (transmitter через пайп) не успевает — новые датаграммы дропаются со +// счётчиком (семантика UDP: контрактный дроп лучше блокировки recv-цикла). +static int mode_listen(int port) { + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { perror("socket"); return 1; } + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons((uint16_t)port); + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); close(fd); return 1; + } + + int fl = fcntl(STDOUT_FILENO, F_GETFL, 0); + fcntl(STDOUT_FILENO, F_SETFL, fl | O_NONBLOCK); + + uint16_t id = 0; + uint64_t sent = 0, dropped = 0, oversize = 0; + uint8_t dgram[GW_MAX_PAYLOAD + 1]; // +1: отличить датаграмму ≥1473 Б от валидной 1472 + uint8_t rec[GW_MAX_RECORD]; + + fprintf(stderr, "udp_gw: слушаю UDP :%d -> stdout (записи <= %d Б)\n", port, GW_MAX_RECORD); + time_t last = time(NULL); + while (!g_stop) { + fd_set rfds; FD_ZERO(&rfds); FD_SET(fd, &rfds); + struct timeval tv = { .tv_sec = 1, .tv_usec = 0 }; + int sel = select(fd + 1, &rfds, NULL, NULL, &tv); + if (sel < 0) { if (errno == EINTR) continue; break; } + if (sel > 0 && FD_ISSET(fd, &rfds)) { + ssize_t n = recvfrom(fd, dgram, sizeof(dgram), 0, NULL, NULL); + if (n < 0) { + if (errno != EINTR && errno != EAGAIN) break; + } else if ((size_t)n > GW_MAX_PAYLOAD) { + oversize++; + } else { + size_t rl = gw_pack(rec, id++, dgram, (uint16_t)n); + ssize_t w = write(STDOUT_FILENO, rec, rl); + if (w < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) dropped++; + else if (errno != EINTR) break; + } else if ((size_t)w < rl) { + dropped++; // частичная запись — парсер на другом конце сам ресинхронизируется + } else { + sent++; + } + } + } + time_t now = time(NULL); + if (now > last) { + fprintf(stderr, "\rотправлено %llu | дропов %llu | негабарит %llu", + (unsigned long long)sent, (unsigned long long)dropped, + (unsigned long long)oversize); + fflush(stderr); + last = now; + } + } + fprintf(stderr, "\nudp_gw (listen) завершён: отправлено %llu | дропов %llu | негабарит %llu\n", + (unsigned long long)sent, (unsigned long long)dropped, (unsigned long long)oversize); + close(fd); + return 0; +} + +// Выход: записи из stdin → UDP-датаграммы на dst. Парсер сам ресинхронизируется +// после дыры в потоке (см. gw_parser_next). +static int mode_deliver(const char *hostport) { + const char *colon = strrchr(hostport, ':'); + if (!colon || colon == hostport) { + fprintf(stderr, "udp_gw: -d ожидает ip:port, получено '%s'\n", hostport); + return 1; + } + char host[64]; + size_t hlen = (size_t)(colon - hostport); + if (hlen >= sizeof(host)) { fprintf(stderr, "udp_gw: адрес слишком длинный\n"); return 1; } + memcpy(host, hostport, hlen); host[hlen] = 0; + int port = atoi(colon + 1); + + struct sockaddr_in dst; + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, host, &dst.sin_addr) != 1) { + fprintf(stderr, "udp_gw: не разобрал IP '%s' (только числовой IPv4)\n", host); + return 1; + } + + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { perror("socket"); return 1; } + + gw_parser_t p; + gw_parser_init(&p); + uint8_t iobuf[GW_IO_CHUNK]; + + fprintf(stderr, "udp_gw: stdin -> UDP %s:%d\n", host, port); + time_t last = time(NULL); + while (!g_stop) { + ssize_t n = read(STDIN_FILENO, iobuf, sizeof(iobuf)); + if (n < 0) { if (errno == EINTR) continue; break; } + if (n == 0) break; // EOF — источник (receiver) завершился + gw_parser_push(&p, iobuf, (size_t)n); + + uint16_t id, len; const uint8_t *pl; + while (gw_parser_next(&p, &id, &pl, &len)) + sendto(fd, pl, len, 0, (struct sockaddr *)&dst, sizeof(dst)); + + time_t now = time(NULL); + if (now > last) { + fprintf(stderr, "\rдоставлено %llu | ресинк %llu Б | bad_crc %llu | дыр(id) %llu", + (unsigned long long)p.records, (unsigned long long)p.resync_bytes, + (unsigned long long)p.bad_crc, (unsigned long long)p.gaps); + fflush(stderr); + last = now; + } + } + fprintf(stderr, "\nudp_gw (deliver) завершён: доставлено %llu | ресинк %llu Б | " + "bad_crc %llu | дыр(id) %llu\n", + (unsigned long long)p.records, (unsigned long long)p.resync_bytes, + (unsigned long long)p.bad_crc, (unsigned long long)p.gaps); + close(fd); + return 0; +} + +static void usage(const char *prog) { + fprintf(stderr, "udp_gw — UDP-туннель поверх pluto-link (roadmap §10 п.8)\n\n"); + fprintf(stderr, "Вход в линк: %s -l ПОРТ | transmitter ...\n", prog); + fprintf(stderr, "Выход из линка: receiver ... | %s -d ip:port\n\n", prog); + fprintf(stderr, " -l ПОРТ слушать UDP на ПОРТ, писать записи в stdout\n"); + fprintf(stderr, " -d ip:port читать записи из stdin, слать UDP на ip:port\n"); + fprintf(stderr, " -h эта справка\n"); +} + +int main(int argc, char *argv[]) { + setup_signals(); + int opt; + int listen_port = -1; + const char *deliver_dst = NULL; + while ((opt = getopt(argc, argv, "l:d:h")) != -1) { + switch (opt) { + case 'l': listen_port = atoi(optarg); break; + case 'd': deliver_dst = optarg; break; + case 'h': usage(argv[0]); return 0; + default: usage(argv[0]); return 1; + } + } + if (listen_port > 0 && !deliver_dst) return mode_listen(listen_port); + if (deliver_dst && listen_port < 0) return mode_deliver(deliver_dst); + usage(argv[0]); + return 1; +} + +#else +// --- Оффлайн-самопроверка парсера: cc -O2 -DGW_SELFTEST udp_gw.c -o gwtest --- +// По духу — как group_fec.c/GF_SELFTEST: чистая логика без сети/iio, нативная +// сборка на хосте (RPi/aarch64), кросс-тулчейн не нужен. + +static int fails = 0; +#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: " __VA_ARGS__); printf("\n"); fails++; } } while (0) + +// Прогоняет payloads[i] (i=0..cnt-1) через pack, склеивает в один поток, +// прогоняет через парсер (кусками по `chunk` байт, 0 = одним куском — +// имитация разной гранулярности read()), сверяет извлечённые записи 1-в-1. +static void run_stream(const char *name, const uint8_t **payloads, const uint16_t *lens, int cnt, + size_t chunk) { + uint8_t *stream = malloc((size_t)cnt * GW_MAX_RECORD); + size_t slen = 0; + for (int i = 0; i < cnt; i++) + slen += gw_pack(stream + slen, (uint16_t)i, payloads[i], lens[i]); + + gw_parser_t p; gw_parser_init(&p); + size_t off = 0; + while (off < slen) { + size_t take = chunk ? (chunk < slen - off ? chunk : slen - off) : slen - off; + gw_parser_push(&p, stream + off, take); + off += take; + } + + int got = 0; + uint16_t id, len; const uint8_t *pl; + while (gw_parser_next(&p, &id, &pl, &len)) { + CHECK(id == got, "%s: id %u != ожид. %d", name, id, got); + CHECK(len == lens[got], "%s: len %u != ожид. %u (rec %d)", name, len, lens[got], got); + CHECK(memcmp(pl, payloads[got], len) == 0, "%s: данные не совпали (rec %d)", name, got); + got++; + } + CHECK(got == cnt, "%s: извлечено %d записей, ожидалось %d", name, got, cnt); + CHECK(p.bad_crc == 0, "%s: ложных bad_crc %llu на чистом потоке", name, (unsigned long long)p.bad_crc); + CHECK(p.gaps == 0, "%s: ложных дыр id %llu на непрерывном потоке", name, (unsigned long long)p.gaps); + free(stream); +} + +int main(void) { + srand(42); + + // 1. Обычный поток, размеры 1 / 1024 / 1472 / случайные, разными чанками read(). + { + uint16_t lens[6] = { 1, 1024, 1472, 37, 500, 1471 }; + uint8_t *bufs[6]; + const uint8_t *pl[6]; + for (int i = 0; i < 6; i++) { + bufs[i] = malloc(lens[i]); + for (int j = 0; j < lens[i]; j++) bufs[i][j] = (uint8_t)(rand() & 0xFF); + pl[i] = bufs[i]; + } + run_stream("одним куском", pl, lens, 6, 0); + run_stream("по 1 байту", pl, lens, 6, 1); + run_stream("по 3 байта", pl, lens, 6, 3); + run_stream("по 4096 Б", pl, lens, 6, 4096); + for (int i = 0; i < 6; i++) free(bufs[i]); + } + + // 2. Дыра посреди потока (потерянный кадр линка на симплексе): вырезаем + // запись #1 целиком → парсер должен ресинхронизироваться на следующей + // и правильно посчитать ровно одну дыру id. + { + uint16_t lens[4] = { 200, 300, 200, 300 }; + uint8_t *bufs[4]; const uint8_t *pl[4]; + for (int i = 0; i < 4; i++) { + bufs[i] = malloc(lens[i]); + for (int j = 0; j < lens[i]; j++) bufs[i][j] = (uint8_t)(rand() & 0xFF); + pl[i] = bufs[i]; + } + uint8_t stream[4 * (300 + GW_HDR)]; + size_t slen = 0; + for (int i = 0; i < 4; i++) slen += gw_pack(stream + slen, (uint16_t)i, pl[i], lens[i]); + + size_t rec1_off = GW_HDR + lens[0]; // где начинается запись #1 + size_t rec1_len = GW_HDR + lens[1]; // её полная длина + uint8_t cut[sizeof(stream)]; + size_t clen = rec1_off; + memcpy(cut, stream, rec1_off); + memcpy(cut + clen, stream + rec1_off + rec1_len, slen - rec1_off - rec1_len); + clen += slen - rec1_off - rec1_len; + + gw_parser_t p; gw_parser_init(&p); + gw_parser_push(&p, cut, clen); + int got = 0; uint16_t exp_ids[3] = { 0, 2, 3 }; + uint16_t id, len; const uint8_t *rpl; + while (gw_parser_next(&p, &id, &rpl, &len)) { + CHECK(id == exp_ids[got], "дыра: id %u != ожид. %u (rec %d)", id, exp_ids[got], got); + got++; + } + CHECK(got == 3, "дыра: извлечено %d записей, ожидалось 3", got); + CHECK(p.gaps == 1, "дыра: gaps %llu != 1 (потеряна ровно запись #1)", (unsigned long long)p.gaps); + for (int i = 0; i < 4; i++) free(bufs[i]); + } + + // 3. Случайный magic в данных (ложное совпадение) — hcrc8 должен отсеять + // ГАРАНТИРОВАННО (crc намеренно испорчен), ресинк не теряет настоящую запись. + { + uint8_t junk[64]; + for (int i = 0; i < 64; i++) junk[i] = (uint8_t)(rand() & 0xFF); + junk[10] = GW_MAGIC0; junk[11] = GW_MAGIC1; + uint8_t want = gw_crc8(junk + 10, 7); + junk[17] = (uint8_t)(want + 1); // намеренно НЕ совпадает с валидным hcrc8 + + uint16_t plen = 50; + uint8_t plbuf[50]; + for (int i = 0; i < 50; i++) plbuf[i] = (uint8_t)(rand() & 0xFF); + + uint8_t stream[64 + GW_HDR + 50]; + memcpy(stream, junk, 64); + size_t slen = 64 + gw_pack(stream + 64, 7, plbuf, plen); + + gw_parser_t p; gw_parser_init(&p); + gw_parser_push(&p, stream, slen); + uint16_t id, len; const uint8_t *rpl; + int ok = gw_parser_next(&p, &id, &rpl, &len); + CHECK(ok == 1, "ложный magic: запись не найдена"); + CHECK(id == 7, "ложный magic: id %u != 7", id); + CHECK(len == plen && memcmp(rpl, plbuf, plen) == 0, "ложный magic: данные не совпали"); + CHECK(p.bad_crc >= 1, "ложный magic: bad_crc не сработал (%llu)", (unsigned long long)p.bad_crc); + } + + // 4. Обрыв хвоста (payload/заголовок не влез целиком) — парсер должен + // вернуть 0 и НЕ прочитать частичные данные как запись, пока хвост не доедет. + { + uint16_t plen = 500; + uint8_t plbuf[500]; + for (int i = 0; i < 500; i++) plbuf[i] = (uint8_t)(rand() & 0xFF); + uint8_t rec[GW_HDR + 500]; + size_t rl = gw_pack(rec, 3, plbuf, plen); + + gw_parser_t p; gw_parser_init(&p); + gw_parser_push(&p, rec, rl - 10); // не хватает 10 последних байт + uint16_t id, len; const uint8_t *rpl; + CHECK(gw_parser_next(&p, &id, &rpl, &len) == 0, "обрыв хвоста: запись найдена преждевременно"); + gw_parser_push(&p, rec + rl - 10, 10); // дослали остаток + CHECK(gw_parser_next(&p, &id, &rpl, &len) == 1, "обрыв хвоста: запись не найдена после докладки"); + CHECK(id == 3 && len == plen, "обрыв хвоста: данные после докладки не совпали"); + } + + // 5. Wrap-around id (u16): 65534,65535,0,1 — без ложных "дыр" на переходе. + { + uint8_t pl0[10] = {0}, pl1[10] = {0}, pl2[10] = {0}, pl3[10] = {0}; + uint8_t stream[4 * (GW_HDR + 10)]; + size_t slen = 0; + slen += gw_pack(stream + slen, 65534, pl0, 10); + slen += gw_pack(stream + slen, 65535, pl1, 10); + slen += gw_pack(stream + slen, 0, pl2, 10); + slen += gw_pack(stream + slen, 1, pl3, 10); + + gw_parser_t p; gw_parser_init(&p); + gw_parser_push(&p, stream, slen); + int got = 0; + uint16_t id, len; const uint8_t *rpl; + while (gw_parser_next(&p, &id, &rpl, &len)) got++; + CHECK(got == 4, "wrap: извлечено %d записей, ожидалось 4", got); + CHECK(p.gaps == 0, "wrap: ложные дыры на переходе через 0: %llu", (unsigned long long)p.gaps); + } + + printf("gwtest: %s\n", fails ? "ПРОВАЛ" : "OK"); + return fails ? 1 : 0; +} +#endif