Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F407 · уровень средний · 26 мин чтения
Открыть в интерактивном справочнике

198ж. Датчик по UDP: широковещание, обнаружение в сети и свой протокол

Разделы 105 и 106 разобрали сеть по уровням, 106а довёл кадры до памяти F407. Здесь — первый сетевой прибор целиком: датчик раз в секунду шлёт измерение на широковещательный адрес подсети или в многоадресную группу, отвечает на вопрос «кто здесь?» своим MAC, IP и версией, а программа на ПК находит все приборы в сети, ведёт журнал и замечает потерянные пакеты по номерам. Протокол свой: 16 байт заголовка, данные и CRC.

Прибор написан на lwIP (raw API, без ОС) и собран двумя способами: для F407 компилятором armclang — ради размеров и предупреждений — и под Linux тем же sensor_app.c, где вместо MAC F407 работает сырой сокет. Три таких прибора и сборщик на python3 живут в одной docker-сети.

Опоры: исходники lwIP 2.1.2 из архива STM32CubeF4 1.28.0 (udp.c, udp.h, opt.h, timeouts.c), RM0090 (дескрипторы Ethernet), stm32f4xx_hal_eth.h и stm32f4xx_ll_adc.h из пака Keil STM32F4xx_DFP. Файлы — _verify/udp198h/: us_proto.h/.c (пакет), sensor_app.c (прибор), port_f407.c (F407), main_linux.c (Linux), collector.py (сборщик), сохранённые прогоны *_host.txt.

Как это называется в документации

Оригинал Перевод
broadcast address широковещательный адрес: все единицы в номере узла
multicast group многоадресная группа: адреса 224.0.0.0–239.255.255.255
IGMP join вступление в группу: узел сообщает сети, что ждёт её пакеты
raw API «сырой» интерфейс lwIP: функции и обработчики без ОС
pbuf буфер пакета lwIP; бывает цепочкой из нескольких кусков
discovery обнаружение приборов в сети
sequence number номер пакета по порядку
checksum offload досчёт контрольной суммы сетевым адаптером

198ж.1 Что делает прибор

Функция Как
измерение раз в 1000 мс: время работы, температура кристалла, питание
куда широковещательный адрес подсети или группа 239.19.8.1, порт 19800
обнаружение на WHO отвечает HERE: MAC, IP, маска, версия, период
потери номер в каждом пакете; кольцо 32 последних измерений; повтор по GET
стек lwIP 2.1.2, NO_SYS=1, только UDP, ICMP, IGMP и ARP
на ПК collector.py: находит приборы, пишет CSV, просит повтор пропусков

Пакет датчика по байтам — заголовок 16 байт, 8 байт данных и CRC, всего 26 — и четыре вида обмена между сборщиком на ПК и тремя приборами в подсети 10.198.8.0/24

Четыре вида пакетов отличаются полем «тип». Измерение (MEAS) уходит всем сразу: сборщиков может быть несколько, и приборам не нужно знать их адреса. Вопрос WHO сборщик шлёт всем, ответ HERE прибор возвращает только спросившему. GET — просьба повторить пакет с заданным номером.

Широковещание — самый простой способ «сказать всем», но у него предел: маршрутизатор его не пропускает (раздел 108). Сборщик и приборы должны быть в одной подсети; для связи через маршрутизатор нужен адрес сборщика и обычная адресная отправка.


198ж.2 Свой пакет: байты, порядок, выравнивание

Формат описан байтами и смещениями, как требует раздел 126.7. Структура в заголовочном файле — только описание из массивов байтов, и проверки _Static_assert не дадут ей разойтись со смещениями:

_Static_assert(sizeof(struct us_hdr_wire) == US_HDR_LEN, "заголовок — 16 байт");
_Static_assert(offsetof(struct us_hdr_wire, seq) == US_OFF_SEQ, "номер — с байта 4");
_Static_assert(offsetof(struct us_hdr_wire, mac) == US_OFF_MAC, "MAC — с байта 8");
_Static_assert(offsetof(struct us_hdr_wire, len) == US_OFF_LEN, "длина — байт 15");
_Static_assert(_Alignof(struct us_hdr_wire) == 1, "массивы байтов: выравнивание 1");
_Static_assert(US_HDR_LEN + US_MEAS_LEN + US_CRC_LEN == 26, "пакет измерения — 26 байт");
_Static_assert(US_HDR_LEN + US_HERE_LEN + US_CRC_LEN == 32, "ответ на WHO — 32 байта");

Сборка — побайтно, старшим байтом вперёд; CRC-16/XMODEM (раздел 194) считается по заголовку и данным:

    put32(out + US_OFF_SEQ, seq);
    memcpy(out + US_OFF_MAC, mac, 6);
    out[US_OFF_FLAGS] = flags;
    out[US_OFF_LEN] = len;
    if (len)
        memcpy(out + US_HDR_LEN, data, len);
    put16(out + US_HDR_LEN + len, us_crc16(out, US_HDR_LEN + (size_t)len));

Измерение номер 300 от прибора 06:42:8F:70:AA:C7 (адрес из раздела 105.3), время работы 300 000 мс, 23,45 °C, 3300 мВ — это 26 байт:

55 53 01 01 00 00 01 2c 06 42 8f 70 aa c7 00 08 00 04 93 e0 09 29 0c e4 a3 64

Номер 300 лежит как 00 00 01 2c. «Естественная» структура с полями uint32_t дала бы те же 24 байта до CRC — поля нарочно стоят на своих границах, заполнения нет, — но номер в памяти Cortex-M и x86 лежит как 2c 01 00 00. Отправить такую структуру целиком — значит отдать порядок байт на волю процессора. Контрольное значение CRC для строки «123456789» — 0x31C3; один перевёрнутый бит в температуре, укороченная датаграмма и чужое магическое слово дают три разных отказа разбора (−5, −4 и −2 в us_parse). Тот же формат на ПК — us_proto.py через struct с форматом >2sBBI6sBB; векторы C и Python совпали байт в байт.

Зачем своя CRC, если у UDP есть сумма? В IPv4 сумма UDP необязательна — ноль в поле значит «суммы нет» (раздел 106.1), — и она слабее CRC. Своя CRC делает пакет самодостаточным: он проверяется одинаково, пришёл ли он по сети, из файла журнала или по UART. А вот COBS из раздела 194 здесь не нужен: границы пакета задаёт сама датаграмма, длина известна.

Выравнивание всплывает при чтении. Прибор под Linux записывал адрес данных принятой датаграммы по модулю 4 — у всех трёх вышло 2: 14 байт заголовка Ethernet сдвигают всё, что дальше. Поэтому поля читаются побайтно. armclang собрал us_rd32 так:

us_rd32:  LDR  r0,[r0,#0]
          REV  r0,r0
          BX   lr

Четыре чтения байта превратились в одно LDR и разворот REV: M4 умеет читать невыровненное слово (раздел 126.3). На M0 тот же исходник даст побайтное чтение — и тоже правильно, а приведение указателя к uint32_t * там упало бы.


198ж.3 Прибор на lwIP raw API

Без ОС (NO_SYS=1) стек крутит главный цикл: принять кадр, провернуть таймеры lwIP, спросить прибор, не пора ли мерить. Отправка — через pbuf:

static void send_to(const uint8_t *b, size_t n, const ip_addr_t *dst, u16_t port)
{
    struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, (u16_t)n, PBUF_RAM);
    if (p == NULL) {                    /* память стека кончилась — пакет пропадёт, как в сети */
        st.no_pbuf++;
        return;
    }
    memcpy(p->payload, b, n);           /* PBUF_RAM — один непрерывный кусок */
    udp_sendto(pcb, p, dst, port);      /* место под заголовки UDP, IP и Ethernet уже оставлено */
    pbuf_free(p);                       /* udp_sendto буфер не освобождает */
}

PBUF_TRANSPORT оставляет перед данными место под заголовки нижних уровней, и lwIP не копирует пакет ещё раз. Документация в udp.c говорит прямо: «The pbuf is not deallocated» — освобождает отправитель. При приёме наоборот: по udp.h буфер освобождает обработчик, а адрес отправителя может указывать внутрь этого буфера. Поэтому обработчик сначала копирует адрес, потом зовёт pbuf_free:

        ip_addr_copy(who_addr, *addr);  /* addr указывает внутрь p: копировать до pbuf_free */
        who_port = port;
        who_seq = pk.seq;               /* ответ несёт номер вопроса */
        who_pending = 1;

Данные читаются pbuf_copy_partial — принятый пакет может лежать цепочкой pbuf, и p->payload — лишь первый кусок.

Широковещательный адрес прибор считает сам — по адресу и маске (раздел 105.5): номер узла заполняется единицами.

        ip_addr_set_ip4_u32(&dst, ip4_addr_get_u32(netif_ip4_addr(nif)) | ~ip4_addr_get_u32(netif_ip4_netmask(nif)));

Под F407 те же файлы садятся на драйвер из 106а: port_f407.c подключает eth106a.c (RMII, MDIO, кольцо приёма), добавляет один дескриптор передачи, часы sys_now от SysTick и чтение датчика температуры и VREFINT по заводским точкам калибровки (адреса 0x1FFF7A2A, 0x1FFF7A2C и 0x1FFF7A2E — из stm32f4xx_ll_adc.h). Длину принятого кадра драйвер берёт из поля FL, а оно по RM0090 включает CRC — четыре байта FCS lwIP не отдаются.

armclang -Os, -Wall -Wextra — ноль предупреждений. В собранном образе (неиспользуемое линкер выбросил):

Часть Код и константы ОЗУ
lwIP: UDP, IPv4, ICMP, IGMP, ARP 9166 байт 12 048 байт
прибор, пакет, порт F407 2266 байт 8287 байт
весь образ 12 572 байта 22 216 байт

ОЗУ lwIP — почти всё пулы из lwipopts.h: шесть буферов приёма по 1536 байт и куча 2 КБ под пакеты на отправку. Своё ОЗУ прибора — в основном буферы Ethernet (четыре на приём и один на передачу по 1536 байт) и кольцо измерений. Во весь образ входят ещё стек и куча из файла запуска.


198ж.4 Обнаружение: «кто здесь?»

Сборщик не знает адресов приборов. Он шлёт WHO на широковещательный адрес — или в группу, — и каждый прибор отвечает HERE: MAC, IP, маска, версия прошивки, период. Если бы все ответили мгновенно, ответы пришли бы одной пачкой и могли бы не поместиться в очереди коммутатора и сборщика. Поэтому прибор откладывает ответ на 0…99 мс — по младшим байтам своего MAC:

        sys_timeout((u32_t)(cfg.mac[4] << 8 | cfg.mac[5]) % 100u, here_later, NULL);

В опыте задержки вышли 19, 8 и 9 мс — ровно 0xAAC7 % 100, 0xAABC % 100 и 0xAABD % 100 для трёх MAC. Ответ несёт номер вопроса: сборщик понимает, на какой из своих WHO пришёл HERE.

Отложенный ответ — это свой таймер sys_timeout, а пул таймеров lwIP считается по включённым модулям: LWIP_NUM_SYS_TIMEOUT_INTERNAL в opt.h — при выключенных в lwipopts.h TCP, DHCP и сборке фрагментов это ARP и IGMP, два. Свой таймер — плюс один:

#define MEMP_NUM_SYS_TIMEOUT    (LWIP_NUM_SYS_TIMEOUT_INTERNAL + 1)

Без этой строки прибор на первом же WHO напечатал «sys_timeout: timeout != NULL, pool MEMP_SYS_TIMEOUT is empty» и больше не ответил ни разу (198ж.7).

Группа вместо широковещания. Прибор вступает в 239.19.8.1 (igmp_joingroup_netif); кадр в группу идёт на MAC 01:00:5E:13:08:01 — младшие 23 бита адреса группы. Выгода в том, что узлы, которые в группу не вступали, отбрасывают такие кадры ещё в MAC, а широковещательный кадр обязан принять и разобрать каждый. У F407 для этого есть фильтр MAC; в port_f407.c он настроен проще всего — «пропускать все групповые» (PAM), отбор делает lwIP. В опыте третий прибор слал измерения в группу, и сборщик получил их наравне с широковещательными.


198ж.5 Потери: номер, повтор, подтверждение

UDP не обещает ни доставки, ни порядка (раздел 106.1). Что с этим делать — решает протокол, и вариантов три:

Способ Что даёт Цена
номер в каждом пакете сборщик знает, сколько и что потеряно 4 байта
повтор по запросу пропуск восстанавливается, если он ещё в кольце кольцо в ОЗУ, пакет GET
подтверждение каждого отправитель знает о каждой доставке обмен на каждый пакет — почти TCP

Для телеметрии разумно первые два: номер — всегда, повтор — для тех пропусков, что нужны. Подтверждение каждого пакета плохо сочетается с широковещанием: кто из сборщиков должен подтверждать? Если нужна гарантированная доставка — это уже TCP или MQTT (раздел 198и).

Сборщик ведёт по каждому MAC ожидаемый номер. Пришёл больший — дырка:

    if d["next"] is not None and pk["seq"] > d["next"]:
        d["gaps"] += 1                                       # дырка в номерах: чего-то не хватает
        for q in range(d["next"], pk["seq"]):
            d["miss"].add(q)
            d["asked"] += 1
            s.sendto(up.build(up.GET, 0, data=struct.pack(">I", q)), src)   # повтор — только нам

Прибор отвечает из кольца на 32 последних измерения: при периоде 1 с это 32 секунды истории. Повтор несёт флаг US_FLAG_REPEAT и исходное время измерения, так что в журнале пропуск встаёт на своё место:

6052,56:2f:8f:70:aa:bc,7,1486696,21.84,3300,0
6052,56:2f:8f:70:aa:bc,6,1485696,21.83,3300,1

Пакет 7 пришёл вместо 6, сборщик в ту же миллисекунду попросил 6 и получил его с временем прибора на 1000 мс раньше. Номер, вытесненный из кольца, прибор не повторяет — он считает такой запрос в get_miss.


198ж.6 Опыт: три прибора и сборщик в docker-сети

QEMU не моделирует Ethernet F407, поэтому прибор гоняется под Linux: main_linux.c открывает на eth0 контейнера сырой сокет AF_PACKET, у lwIP свои MAC и IP, ядро контейнера в обмене не участвует. Сеть 10.198.8.0/24, три прибора (.101, .102, .103) и сборщик (.10). Потерю делает сам прибор — второй пропускает каждое 7-е измерение, третий каждое 10-е, но кладёт их в кольцо: для сборщика это ровно пропажа в сети. tc netem в образе нет. Через 5 с после старта ядро одного из контейнеров шлёт на порт 19800 чужую датаграмму «hello».

Прогон 45 с (run_host.txt):

Прибор Куда Принято Дырок Повторено Потеряно
06:42:8f:70:aa:c7 10.198.8.255 45 0 0 0
56:2f:8f:70:aa:bc 10.198.8.255 39 6 6 0
0a:31:8f:70:aa:bd 239.19.8.1 41 4 4 0

Одна находка по дороге. Первый прогон молчал: приборы не видели ни WHO, ни GET, хотя кадры приходили. Ядро Linux отдаёт в виртуальный кабель датаграммы с недосчитанной суммой UDP — её досчитал бы сетевой адаптер, а в поле пока лежит только сумма псевдозаголовка. lwIP проверяет сумму (CHECKSUM_CHECK_UDP) и молча выбрасывает такие пакеты. main_linux.c досчитывает сумму там, где ядро пометило её незаконченной, — как было бы на проводе. На F407 с настоящим проводом этого нет, но урок общий: захват на передающей машине показывает не то, что ушло в сеть.


198ж.7 Грабли

Прибор ответил на WHO один раз — и больше никогда. Пул таймеров lwIP рассчитан только на свои модули; sys_timeout для отложенного ответа не нашёл места, ответ не завёлся, флаг «жду ответа» остался поднятым. Каждый свой sys_timeout — плюс один в MEMP_NUM_SYS_TIMEOUT.

Сборщик на Python: sendto на широковещательный адрес — «Permission denied». Без SO_BROADCAST ядро не даёт слать на широковещательный адрес (проверено: errno 13). lwIP по умолчанию разрешает сразу: IP_SOF_BROADCAST в opt.h равен 0; если его включить, pcb понадобится SOF_BROADCAST.

Обработчик приёма иногда отвечает не тому. Адрес отправителя лежит внутри принятого pbuf; после pbuf_free его место занимает следующий пакет. Копировать ip_addr_copy до освобождения.

Прибор со временем перестаёт отправлять. Отправитель забыл pbuf_free после udp_sendto — функция буфер не освобождает, куча стека тает, и pbuf_alloc начинает возвращать NULL.

Разбор падает на M0, а на M4 работал. Поля читались приведением указателя, а данные датаграммы лежат со сдвигом 2 от границы слова. Только побайтное чтение или memcpy (раздел 126).

Сборщик за маршрутизатором не видит ни одного прибора. Широковещание дальше подсети не идёт (раздел 108). Сборщик — в той же подсети, либо прибору задают его адрес.

Приборы не видят пакеты соседнего контейнера. Недосчитанная сумма UDP на виртуальном кабеле (198ж.6): lwIP выбрасывает датаграмму без единого сообщения.

У socket в Python 3.11 нет IP_PKTINFO. Константа есть в ядре Linux (8), но не в модуле; collector.py берёт её через getattr со значением по умолчанию.


198ж.8 Итоги

Полные исходники

us_proto.h — формат пакета датчика.

/* 198ж: свой двоичный пакет датчика поверх UDP.
   Формат описан байтами, а не структурой: старший байт вперёд, без выравнивания.
   Один и тот же файл собирается для F407 (armclang) и для Linux (gcc). */
#ifndef US_PROTO_H
#define US_PROTO_H

#include <stddef.h>
#include <stdint.h>

#define US_PORT      19800u             /* порт прибора и сборщика */
#define US_MAGIC0    0x55u              /* 'U' */
#define US_MAGIC1    0x53u              /* 'S' — «UDP Sensor» */
#define US_VERSION   1u

enum us_type {
    US_MEAS = 1,                        /* измерение: прибор → все */
    US_WHO  = 2,                        /* «кто здесь?»: ПК → все */
    US_HERE = 3,                        /* ответ на WHO: прибор → спросившему */
    US_GET  = 4                         /* «повтори пакет N»: ПК → прибору */
};

#define US_FLAG_REPEAT 0x01u            /* пакет — повтор из кольца по запросу GET */

/* смещения полей заголовка */
enum {
    US_OFF_MAGIC = 0,                   /* 2 байта: 'U' 'S' */
    US_OFF_VER   = 2,                   /* 1: версия формата */
    US_OFF_TYPE  = 3,                   /* 1: enum us_type */
    US_OFF_SEQ   = 4,                   /* 4: номер пакета, старшим байтом вперёд */
    US_OFF_MAC   = 8,                   /* 6: MAC прибора — ключ в журнале */
    US_OFF_FLAGS = 14,                  /* 1: US_FLAG_* */
    US_OFF_LEN   = 15,                  /* 1: длина данных после заголовка */
    US_HDR_LEN   = 16
};

#define US_CRC_LEN    2u                /* CRC-16/XMODEM всего, что до неё */
#define US_MEAS_LEN   8u                /* uptime_ms 4, temp_c100 2, vdd_mv 2 */
#define US_HERE_LEN   14u               /* ip 4, mask 4, fw 3, запас 1, period_ms 2 */
#define US_GET_LEN    4u                /* номер нужного пакета */
#define US_MAX_PACKET (US_HDR_LEN + 32u + US_CRC_LEN)

/* описание заголовка массивами байтов: выравнивание 1, заполнения нет */
struct us_hdr_wire {
    uint8_t magic[2];
    uint8_t ver;
    uint8_t type;
    uint8_t seq[4];
    uint8_t mac[6];
    uint8_t flags;
    uint8_t len;
};

_Static_assert(sizeof(struct us_hdr_wire) == US_HDR_LEN, "заголовок — 16 байт");
_Static_assert(offsetof(struct us_hdr_wire, seq) == US_OFF_SEQ, "номер — с байта 4");
_Static_assert(offsetof(struct us_hdr_wire, mac) == US_OFF_MAC, "MAC — с байта 8");
_Static_assert(offsetof(struct us_hdr_wire, len) == US_OFF_LEN, "длина — байт 15");
_Static_assert(_Alignof(struct us_hdr_wire) == 1, "массивы байтов: выравнивание 1");
_Static_assert(US_HDR_LEN + US_MEAS_LEN + US_CRC_LEN == 26, "пакет измерения — 26 байт");
_Static_assert(US_HDR_LEN + US_HERE_LEN + US_CRC_LEN == 32, "ответ на WHO — 32 байта");

struct us_meas {
    uint32_t uptime_ms;
    int16_t temp_c100;                  /* температура в сотых градуса */
    uint16_t vdd_mv;                    /* питание, мВ */
};

struct us_here {
    uint8_t ip[4], mask[4];
    uint8_t fw[3];                      /* версия прошивки: старший, младший, правка */
    uint16_t period_ms;
};

struct us_pkt {                         /* разобранный пакет */
    uint8_t type, flags, len;
    uint32_t seq;
    uint8_t mac[6];
    const uint8_t *data;                /* данные внутри принятого буфера */
};

uint16_t us_crc16(const uint8_t *p, size_t n);
size_t us_build(uint8_t *out, uint8_t type, uint32_t seq, const uint8_t mac[6], uint8_t flags,
                const uint8_t *data, uint8_t len);
size_t us_build_meas(uint8_t *out, uint32_t seq, const uint8_t mac[6], uint8_t flags, const struct us_meas *m);
size_t us_build_here(uint8_t *out, uint32_t seq, const uint8_t mac[6], const struct us_here *h);
int us_parse(const uint8_t *p, size_t n, struct us_pkt *pk);
void us_get_meas(const struct us_pkt *pk, struct us_meas *m);
uint32_t us_rd32(const uint8_t *p);

#endif

us_proto.c — сборка и разбор пакета побайтно.

/* 198ж: сборка и разбор пакета датчика — побайтно, старшим байтом вперёд. */
#include <string.h>
#include "us_proto.h"

uint16_t us_crc16(const uint8_t *p, size_t n)       /* CRC-16/XMODEM, как в разделе 194 */
{
    uint16_t c = 0;
    while (n--) {
        c ^= (uint16_t)(*p++ << 8);
        for (int i = 0; i < 8; i++)
            c = (uint16_t)((c & 0x8000u) ? ((unsigned)c << 1) ^ 0x1021u : (unsigned)c << 1);
    }
    return c;
}

static void put16(uint8_t *p, uint16_t v)
{
    p[0] = (uint8_t)(v >> 8);
    p[1] = (uint8_t)v;
}

static void put32(uint8_t *p, uint32_t v)
{
    p[0] = (uint8_t)(v >> 24);
    p[1] = (uint8_t)(v >> 16);
    p[2] = (uint8_t)(v >> 8);
    p[3] = (uint8_t)v;
}

uint32_t us_rd32(const uint8_t *p)
{
    return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}

static uint16_t rd16(const uint8_t *p)
{
    return (uint16_t)(p[0] << 8 | p[1]);
}

size_t us_build(uint8_t *out, uint8_t type, uint32_t seq, const uint8_t mac[6], uint8_t flags,
                const uint8_t *data, uint8_t len)
{
    out[US_OFF_MAGIC] = US_MAGIC0;
    out[US_OFF_MAGIC + 1] = US_MAGIC1;
    out[US_OFF_VER] = US_VERSION;
    out[US_OFF_TYPE] = type;
    put32(out + US_OFF_SEQ, seq);
    memcpy(out + US_OFF_MAC, mac, 6);
    out[US_OFF_FLAGS] = flags;
    out[US_OFF_LEN] = len;
    if (len)
        memcpy(out + US_HDR_LEN, data, len);
    put16(out + US_HDR_LEN + len, us_crc16(out, US_HDR_LEN + (size_t)len));
    return US_HDR_LEN + (size_t)len + US_CRC_LEN;
}

size_t us_build_meas(uint8_t *out, uint32_t seq, const uint8_t mac[6], uint8_t flags, const struct us_meas *m)
{
    uint8_t d[US_MEAS_LEN];
    put32(d, m->uptime_ms);
    put16(d + 4, (uint16_t)m->temp_c100);          /* знак — в дополнительном коде */
    put16(d + 6, m->vdd_mv);
    return us_build(out, US_MEAS, seq, mac, flags, d, sizeof d);
}

size_t us_build_here(uint8_t *out, uint32_t seq, const uint8_t mac[6], const struct us_here *h)
{
    uint8_t d[US_HERE_LEN];
    memcpy(d, h->ip, 4);
    memcpy(d + 4, h->mask, 4);
    memcpy(d + 8, h->fw, 3);
    d[11] = 0;
    put16(d + 12, h->period_ms);
    return us_build(out, US_HERE, seq, mac, 0, d, sizeof d);
}

/* 0 — пакет цел; отрицательное — почему выброшен */
int us_parse(const uint8_t *p, size_t n, struct us_pkt *pk)
{
    if (n < US_HDR_LEN + US_CRC_LEN)
        return -1;                                  /* короче пустого пакета */
    if (p[US_OFF_MAGIC] != US_MAGIC0 || p[US_OFF_MAGIC + 1] != US_MAGIC1)
        return -2;                                  /* чужой пакет на нашем порту */
    if (p[US_OFF_VER] != US_VERSION)
        return -3;                                  /* версия, которую мы не знаем */
    if (n != US_HDR_LEN + (size_t)p[US_OFF_LEN] + US_CRC_LEN)
        return -4;                                  /* длина не сходится с датаграммой */
    if (us_crc16(p, n - US_CRC_LEN) != rd16(p + n - US_CRC_LEN))
        return -5;
    pk->type = p[US_OFF_TYPE];
    pk->flags = p[US_OFF_FLAGS];
    pk->len = p[US_OFF_LEN];
    pk->seq = us_rd32(p + US_OFF_SEQ);
    memcpy(pk->mac, p + US_OFF_MAC, 6);
    pk->data = p + US_HDR_LEN;
    return 0;
}

void us_get_meas(const struct us_pkt *pk, struct us_meas *m)
{
    m->uptime_ms = us_rd32(pk->data);
    m->temp_c100 = (int16_t)rd16(pk->data + 4);
    m->vdd_mv = rd16(pk->data + 6);
}

sensor_app.c — прибор на lwIP raw API.

/* 198ж: прибор на lwIP raw API (NO_SYS=1): измерение раз в период на широковещательный
   адрес подсети или в группу, ответ на «кто здесь?», повтор пакета по номеру. */
#include <string.h>
#include "lwip/igmp.h"
#include "lwip/pbuf.h"
#include "lwip/sys.h"
#include "lwip/timeouts.h"
#include "lwip/udp.h"
#include "sensor_app.h"

static struct udp_pcb *pcb;
static struct netif *nif;
static struct sensor_cfg cfg;
static struct sensor_stats st;
static uint32_t seq, next_ms;
static struct { uint32_t seq; struct us_meas m; } ring[US_RING];

static ip_addr_t who_addr;              /* кому ответить на WHO — копия, не указатель в pbuf */
static u16_t who_port;
static uint32_t who_seq;
static uint8_t who_pending;

static void send_to(const uint8_t *b, size_t n, const ip_addr_t *dst, u16_t port)
{
    struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, (u16_t)n, PBUF_RAM);
    if (p == NULL) {                    /* память стека кончилась — пакет пропадёт, как в сети */
        st.no_pbuf++;
        return;
    }
    memcpy(p->payload, b, n);           /* PBUF_RAM — один непрерывный кусок */
    udp_sendto(pcb, p, dst, port);      /* место под заголовки UDP, IP и Ethernet уже оставлено */
    pbuf_free(p);                       /* udp_sendto буфер не освобождает */
}

static void here_later(void *arg)
{
    struct us_here h;
    uint8_t b[US_MAX_PACKET];
    (void)arg;
    memcpy(h.ip, netif_ip4_addr(nif), 4);
    memcpy(h.mask, netif_ip4_netmask(nif), 4);
    memcpy(h.fw, cfg.fw, 3);
    h.period_ms = cfg.period_ms;
    send_to(b, us_build_here(b, who_seq, cfg.mac, &h), &who_addr, who_port);
    who_pending = 0;
    st.here++;
}

static void on_recv(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
    uint8_t b[US_MAX_PACKET];
    struct us_pkt pk;
    (void)arg;
    (void)upcb;
    st.rx_payload_mod4 = (uint32_t)((uintptr_t)p->payload & 3u);
    u16_t n = pbuf_copy_partial(p, b, sizeof b, 0);     /* данные могут лежать в цепочке pbuf */
    if (p->tot_len > sizeof b || us_parse(b, n, &pk) != 0) {
        st.bad++;
    } else if (pk.type == US_WHO && !who_pending) {
        ip_addr_copy(who_addr, *addr);  /* addr указывает внутрь p: копировать до pbuf_free */
        who_port = port;
        who_seq = pk.seq;               /* ответ несёт номер вопроса */
        who_pending = 1;
        /* ответы разнесены по времени: 0…99 мс по младшим байтам MAC */
        sys_timeout((u32_t)(cfg.mac[4] << 8 | cfg.mac[5]) % 100u, here_later, NULL);
    } else if (pk.type == US_GET && pk.len == US_GET_LEN) {
        uint32_t want = us_rd32(pk.data);
        if (ring[want % US_RING].seq == want && want < seq) {
            uint8_t o[US_MAX_PACKET];
            size_t k = us_build_meas(o, want, cfg.mac, US_FLAG_REPEAT, &ring[want % US_RING].m);
            send_to(o, k, addr, port);  /* повтор — только спросившему */
            st.repeats++;
        } else {
            st.get_miss++;              /* из кольца уже вытеснен */
        }
    }
    pbuf_free(p);                       /* принятый буфер освобождает обработчик */
}

void sensor_start(struct netif *n, const struct sensor_cfg *c)
{
    ip4_addr_t grp;
    nif = n;
    cfg = *c;
    for (unsigned i = 0; i < US_RING; i++)
        ring[i].seq = 0xFFFFFFFFu;      /* пустая ячейка */
    ip4_addr_set_u32(&grp, PP_HTONL(US_MCAST));
    igmp_joingroup_netif(nif, &grp);    /* WHO может прийти и в группу */
    pcb = udp_new();
    udp_bind(pcb, IP_ANY_TYPE, US_PORT);
    udp_recv(pcb, on_recv, NULL);
    next_ms = sys_now() + cfg.period_ms;
}

void sensor_poll(uint32_t now_ms)
{
    ip_addr_t dst;
    struct us_meas m;
    uint8_t b[US_MAX_PACKET];
    if ((int32_t)(now_ms - next_ms) < 0)
        return;                         /* сравнение по знаку разности: переживает переполнение */
    next_ms += cfg.period_ms;
    sensor_read(&m);
    ring[seq % US_RING].seq = seq;
    ring[seq % US_RING].m = m;
    if (cfg.meas_to_mcast)
        ip_addr_set_ip4_u32(&dst, PP_HTONL(US_MCAST));
    else                                /* широковещательный адрес подсети: узел — все единицы */
        ip_addr_set_ip4_u32(&dst, ip4_addr_get_u32(netif_ip4_addr(nif)) | ~ip4_addr_get_u32(netif_ip4_netmask(nif)));
    if (cfg.skip_every && seq % cfg.skip_every == cfg.skip_every - 1u) {
        st.skipped++;                   /* опыт: «потерян в сети», но лежит в кольце */
    } else {
        send_to(b, us_build_meas(b, seq, cfg.mac, 0, &m), &dst, US_PORT);
        st.sent++;
    }
    seq++;
}

const struct sensor_stats *sensor_get_stats(void)
{
    return &st;
}

collector.py — сборщик на компьютере.

# -*- coding: utf-8 -*-
"""198ж: сборщик на ПК — находит приборы в сети, пишет журнал измерений, замечает пропуски
по номерам и просит повтор (GET). Только стандартная библиотека.

python3 collector.py <широковещательный адрес> <группа> <секунд> <журнал.csv>"""
import socket, struct, sys, time
import us_proto as up

BCAST, GROUP, RUN, LOG = sys.argv[1], sys.argv[2], float(sys.argv[3]), sys.argv[4]

IP_PKTINFO = getattr(socket, "IP_PKTINFO", 8)          # в Linux — 8; в модуле socket его может не быть
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)     # без него sendto на .255 — «Permission denied»
s.setsockopt(socket.IPPROTO_IP, IP_PKTINFO, 1)       # узнать, на какой адрес пришла датаграмма
s.bind(("", up.PORT))
s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
             socket.inet_aton(GROUP) + socket.inet_aton("0.0.0.0"))
s.settimeout(0.05)

t0 = time.monotonic()
now_ms = lambda: (time.monotonic() - t0) * 1000.0
found, devs, bad, own = {}, {}, {}, 0
log = open(LOG, "w")
log.write("t_ms,mac,seq,uptime_ms,temp_c,vdd_mv,repeat\n")
plan = [(1000, "who", BCAST, 0x198), (2000, "who", GROUP, 0x199)]
who_sent, old_get = {}, None


def send(pkt, addr):
    s.sendto(pkt, (addr, up.PORT))


while now_ms() < RUN * 1000:
    while plan and now_ms() >= plan[0][0]:
        _, _, addr, q = plan.pop(0)
        send(up.build(up.WHO, q), addr)
        who_sent[q] = (now_ms(), addr)
    if old_get is None and now_ms() >= RUN * 1000 - 1500:
        old_get = now_ms()                                   # номер 0 давно вытеснен из кольца
        for d in devs.values():
            s.sendto(up.build(up.GET, 0, data=struct.pack(">I", 0)), d["addr"])
    try:
        p, anc, _, src = s.recvmsg(256, 64)
    except socket.timeout:
        continue
    dst = "?"
    for lvl, typ, data in anc:
        if lvl == socket.IPPROTO_IP and typ == IP_PKTINFO:
            dst = socket.inet_ntoa(data[8:12])               # in_pktinfo.ipi_addr — адрес из заголовка IP
    pk, why = up.parse(p)
    if pk is None:
        bad[why] = bad.get(why, 0) + 1
        continue
    if pk["type"] in (up.WHO, up.GET):
        own += 1                                             # свой же вопрос вернулся петлёй
        continue
    mac = up.mac_str(pk["mac"])
    if pk["type"] == up.HERE:
        ip, mask, fw, per = up.here(pk["data"])
        t_who, via = who_sent[pk["seq"]]
        found.setdefault(mac, []).append("here mac=%s ip=%s mask=%s fw=%d.%d.%d period=%d who=%s delay_ms=%.0f" % (
            mac, socket.inet_ntoa(ip), socket.inet_ntoa(mask), fw[0], fw[1], fw[2], per, via, now_ms() - t_who))
        continue
    up_ms, t100, vdd = up.meas(pk["data"])
    rep = pk["flags"] & up.FLAG_REPEAT
    d = devs.setdefault(mac, dict(addr=src, next=None, n=0, first=pk["seq"], last=0, gaps=0, miss=set(), asked=0,
                                  rec=0, dup=0, dst=set(), old=0))
    log.write("%.0f,%s,%d,%d,%.2f,%d,%d\n" % (now_ms(), mac, pk["seq"], up_ms, t100 / 100.0, vdd, rep))
    if rep:
        if pk["seq"] in d["miss"]:
            d["miss"].discard(pk["seq"])
            d["rec"] += 1
        elif pk["seq"] == 0 and old_get is not None:
            d["old"] += 1
        continue
    d["n"] += 1
    d["dst"].add(dst)
    if d["next"] is not None and pk["seq"] > d["next"]:
        d["gaps"] += 1                                       # дырка в номерах: чего-то не хватает
        for q in range(d["next"], pk["seq"]):
            d["miss"].add(q)
            d["asked"] += 1
            s.sendto(up.build(up.GET, 0, data=struct.pack(">I", q)), src)   # повтор — только нам
    elif d["next"] is not None and pk["seq"] < d["next"]:
        d["dup"] += 1                                        # старый номер: повтор или перестановка
        continue
    d["next"] = pk["seq"] + 1
    d["last"] = pk["seq"]

log.close()
print("found %d devices" % len(found))
for mac in sorted(found):
    for line in found[mac]:
        print(line)
tot = dict(n=0, gaps=0, asked=0, rec=0, lost=0)
for mac in sorted(devs):
    d = devs[mac]
    print("dev mac=%s from=%s:%d to=%s meas=%d seq=%d..%d gaps=%d asked=%d recovered=%d lost=%d dup=%d old_get_answers=%d" % (
        mac, d["addr"][0], d["addr"][1], ",".join(sorted(d["dst"])), d["n"], d["first"], d["last"], d["gaps"],
        d["asked"], d["rec"], len(d["miss"]), d["dup"], d["old"]))
    for k in ("n", "gaps", "asked", "rec"):
        tot[k] += d[k]
    tot["lost"] += len(d["miss"])
print("total meas=%(n)d gaps=%(gaps)d asked=%(asked)d recovered=%(rec)d lost=%(lost)d" % tot,
      "own_loopback=%d bad=%s" % (own, sorted(bad.items())))

Дальше: раздел 198з «Веб-страница и REST на STM32: lwIP httpd, SSI и CGI» — прибор в сети есть; теперь к нему можно зайти браузером.

Открыть в интерактивном справочнике