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, просит повтор пропусков |
Четыре вида пакетов отличаются полем «тип». Измерение (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 |
- Сборщик нашёл 3 прибора — и по широковещательному WHO, и по WHO в группу; каждый ответил дважды.
- 125 измерений, 10 дырок, 10 повторов, потерь 0. В журнале 135 строк: 125 измерений и 10 повторов.
- Запрос номера 0 в конце прогона не повторил ни один прибор: номер
давно вытеснен из кольца, у каждого
get_miss=1. - «hello» сборщик отверг как слишком короткую, каждый прибор — тоже
(
bad=1). - Групповые кадры пришли на MAC 01:00:5e:13:08:01.
Одна находка по дороге. Первый прогон молчал: приборы не видели ни 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 Итоги
- Пакет — 16 байт заголовка (магическое слово, версия, тип, номер, MAC,
флаги, длина), данные и CRC-16/XMODEM: измерение 26 байт, WHO 18,
HERE 32, GET 22; порядок — старшим байтом вперёд,
_Static_assertдержит смещения. - Данные принятой датаграммы лежат со сдвигом 2 от границы слова;
побайтное чтение armclang превратил в
LDR+REV. - lwIP 2.1.2 без ОС, только UDP: 9166 байт кода и 12 048 байт ОЗУ; образ прибора для F407 — 12 572 байта Flash и 22 216 байт ОЗУ, ноль предупреждений.
- Обнаружение: WHO всем, HERE спросившему через 0…99 мс по MAC — в опыте
19, 8 и 9 мс; свой таймер — плюс один в
MEMP_NUM_SYS_TIMEOUT. - Потери: номер в каждом пакете, кольцо на 32 измерения, повтор по GET — в опыте 10 дырок из 10 закрыты, 125 измерений от 3 приборов за 45 с.
- Широковещание не проходит через маршрутизатор: сборщик должен жить в подсети приборов.
Полные исходники
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» — прибор в сети есть; теперь к нему можно зайти браузером.